Skip to main content

infigraph_core/graph/
queries.rs

1use anyhow::Result;
2use kuzu::Connection;
3use serde::Serialize;
4
5/// High-level graph query interface for analysis.
6pub struct GraphQuery<'a, 'db> {
7    conn: &'a Connection<'db>,
8}
9
10impl<'a, 'db> GraphQuery<'a, 'db> {
11    pub fn new(conn: &'a Connection<'db>) -> Self {
12        Self { conn }
13    }
14
15    /// Find all symbols in a file.
16    pub fn symbols_in_file(&self, file: &str) -> Result<Vec<SymbolRow>> {
17        let query = format!(
18            "MATCH (s:Symbol) WHERE s.file = '{}' RETURN s.id, s.name, s.kind, s.start_line, s.end_line ORDER BY s.start_line",
19            file.replace('\'', "\\'")
20        );
21        let result = self
22            .conn
23            .query(&query)
24            .map_err(|e| anyhow::anyhow!("query failed: {e}"))?;
25
26        let mut rows = Vec::new();
27        for row in result {
28            if row.len() >= 5 {
29                rows.push(SymbolRow {
30                    id: row[0].to_string(),
31                    name: row[1].to_string(),
32                    kind: row[2].to_string(),
33                    start_line: row[3].to_string().parse().unwrap_or(0),
34                    end_line: row[4].to_string().parse().unwrap_or(0),
35                });
36            }
37        }
38        Ok(rows)
39    }
40
41    /// Find direct callers of a symbol.
42    pub fn callers_of(&self, symbol_id: &str) -> Result<Vec<String>> {
43        let query = format!(
44            "MATCH (caller:Symbol)-[:CALLS]->(target:Symbol) WHERE target.id = '{}' RETURN caller.id",
45            symbol_id.replace('\'', "\\'")
46        );
47        self.collect_strings(&query)
48    }
49
50    /// Find what a symbol calls.
51    pub fn callees_of(&self, symbol_id: &str) -> Result<Vec<String>> {
52        let query = format!(
53            "MATCH (source:Symbol)-[:CALLS]->(callee:Symbol) WHERE source.id = '{}' RETURN callee.id",
54            symbol_id.replace('\'', "\\'")
55        );
56        self.collect_strings(&query)
57    }
58
59    pub fn branches_of(&self, symbol_id: &str) -> Result<Vec<BranchInfo>> {
60        let query = format!(
61            "MATCH (s:Symbol)-[:HAS_STATEMENT]->(st:Statement) WHERE s.id = '{}' \
62             RETURN st.kind, st.condition, st.start_line, st.depth ORDER BY st.start_line",
63            symbol_id.replace('\'', "\\'")
64        );
65        let mut result = self.conn.query(&query)
66            .map_err(|e| anyhow::anyhow!("branches_of failed: {e}"))?;
67        let mut branches = Vec::new();
68        while let Some(row) = result.next() {
69            if row.len() >= 4 {
70                branches.push(BranchInfo {
71                    kind: row[0].to_string(),
72                    condition: row[1].to_string(),
73                    line: row[2].to_string().parse().unwrap_or(0),
74                    depth: row[3].to_string().parse().unwrap_or(0),
75                });
76            }
77        }
78        Ok(branches)
79    }
80
81    /// Transitive impact: all symbols affected by a change to the given symbol.
82    /// Follows CALLS edges in reverse (who calls this, who calls those, etc.).
83    pub fn transitive_impact(&self, symbol_id: &str, max_depth: u32) -> Result<Vec<ImpactRow>> {
84        let query = format!(
85            "MATCH (changed:Symbol)<-[:CALLS* 1..{}]-(affected:Symbol) WHERE changed.id = '{}' RETURN DISTINCT affected.id, affected.name, affected.file, affected.kind",
86            max_depth,
87            symbol_id.replace('\'', "\\'")
88        );
89        let result = self
90            .conn
91            .query(&query)
92            .map_err(|e| anyhow::anyhow!("query failed: {e}"))?;
93
94        let mut rows = Vec::new();
95        for row in result {
96            if row.len() >= 4 {
97                rows.push(ImpactRow {
98                    id: row[0].to_string(),
99                    name: row[1].to_string(),
100                    file: row[2].to_string(),
101                    kind: row[3].to_string(),
102                });
103            }
104        }
105        Ok(rows)
106    }
107
108    /// Find symbols in a file whose line range overlaps [start, end].
109    pub fn symbols_in_range(&self, file: &str, start: u32, end: u32) -> Result<Vec<SymbolDetail>> {
110        let query = format!(
111            "MATCH (s:Symbol) WHERE s.file = '{}' AND s.start_line <= {} AND s.end_line >= {} RETURN s.id, s.name, s.kind, s.file, s.start_line, s.end_line ORDER BY s.start_line",
112            file.replace('\'', "\\'"),
113            end,
114            start
115        );
116        let result = self
117            .conn
118            .query(&query)
119            .map_err(|e| anyhow::anyhow!("query failed: {e}"))?;
120
121        let mut rows = Vec::new();
122        for row in result {
123            if row.len() >= 6 {
124                rows.push(SymbolDetail {
125                    id: row[0].to_string(),
126                    name: row[1].to_string(),
127                    kind: row[2].to_string(),
128                    file: row[3].to_string(),
129                    start_line: row[4].to_string().parse().unwrap_or(0),
130                    end_line: row[5].to_string().parse().unwrap_or(0),
131                });
132            }
133        }
134        Ok(rows)
135    }
136
137    /// Look up a symbol by its ID and return its file, start_line, end_line.
138    pub fn find_symbol_by_id(&self, symbol_id: &str) -> Result<Option<SymbolDetail>> {
139        let query = format!(
140            "MATCH (s:Symbol) WHERE s.id = '{}' RETURN s.id, s.name, s.kind, s.file, s.start_line, s.end_line",
141            symbol_id.replace('\'', "\\'")
142        );
143        let mut result = self
144            .conn
145            .query(&query)
146            .map_err(|e| anyhow::anyhow!("query failed: {e}"))?;
147
148        if let Some(row) = result.next() {
149            if row.len() >= 6 {
150                return Ok(Some(SymbolDetail {
151                    id: row[0].to_string(),
152                    name: row[1].to_string(),
153                    kind: row[2].to_string(),
154                    file: row[3].to_string(),
155                    start_line: row[4].to_string().parse().unwrap_or(0),
156                    end_line: row[5].to_string().parse().unwrap_or(0),
157                }));
158            }
159        }
160        Ok(None)
161    }
162
163    /// Find all reference locations for a symbol — file, line, column, and calling symbol.
164    /// Returns every place the symbol is called/used, for rename/refactor workflows.
165    pub fn find_all_references(&self, symbol_id: &str) -> Result<Vec<ReferenceRow>> {
166        let q = format!(
167            "MATCH (caller:Symbol)-[:CALLS]->(target:Symbol) \
168             WHERE target.id = '{}' \
169             RETURN caller.id, caller.name, caller.file, caller.start_line, target.id",
170            symbol_id.replace('\'', "\\'")
171        );
172        let result = self
173            .conn
174            .query(&q)
175            .map_err(|e| anyhow::anyhow!("query failed: {e}"))?;
176        let mut rows = Vec::new();
177        for row in result {
178            if row.len() >= 5 {
179                rows.push(ReferenceRow {
180                    caller_id: row[0].to_string(),
181                    caller_name: row[1].to_string(),
182                    file: row[2].to_string(),
183                    line: row[3].to_string().parse().unwrap_or(0),
184                    target_id: row[4].to_string(),
185                });
186            }
187        }
188        Ok(rows)
189    }
190
191    /// Get the public API surface: all public symbols + all routes.
192    pub fn get_api_surface(&self) -> Result<Vec<ApiSymbol>> {
193        let q = "MATCH (s:Symbol) \
194                 WHERE s.visibility = 'public' OR s.kind = 'Route' \
195                 RETURN s.id, s.name, s.kind, s.file, s.start_line, s.visibility, s.docstring \
196                 ORDER BY s.file, s.start_line";
197        let result = self
198            .conn
199            .query(q)
200            .map_err(|e| anyhow::anyhow!("query failed: {e}"))?;
201        let mut rows = Vec::new();
202        for row in result {
203            if row.len() >= 7 {
204                rows.push(ApiSymbol {
205                    id: row[0].to_string(),
206                    name: row[1].to_string(),
207                    kind: row[2].to_string(),
208                    file: row[3].to_string(),
209                    line: row[4].to_string().parse().unwrap_or(0),
210                    visibility: row[5].to_string(),
211                    docstring: row[6].to_string(),
212                });
213            }
214        }
215        Ok(rows)
216    }
217
218    /// Get file-level dependency graph: what this file imports and what imports it.
219    pub fn get_file_deps(&self, file: &str) -> Result<FileDeps> {
220        let esc = file.replace('\'', "\\'");
221
222        // Files this file imports (outgoing)
223        let q_out = format!(
224            "MATCH (m:Module)-[:IMPORTS]->(dep:Module) WHERE m.file = '{}' RETURN dep.file",
225            esc
226        );
227        let r = self
228            .conn
229            .query(&q_out)
230            .map_err(|e| anyhow::anyhow!("query failed: {e}"))?;
231        let mut imports = Vec::new();
232        for row in r {
233            if let Some(v) = row.first() {
234                let s = v.to_string().trim_matches('"').to_string();
235                if !s.is_empty() {
236                    imports.push(s);
237                }
238            }
239        }
240
241        // Files that import this file (incoming)
242        let q_in = format!(
243            "MATCH (m:Module)-[:IMPORTS]->(dep:Module) WHERE dep.file = '{}' RETURN m.file",
244            esc
245        );
246        let r2 = self
247            .conn
248            .query(&q_in)
249            .map_err(|e| anyhow::anyhow!("query failed: {e}"))?;
250        let mut imported_by = Vec::new();
251        for row in r2 {
252            if let Some(v) = row.first() {
253                let s = v.to_string().trim_matches('"').to_string();
254                if !s.is_empty() {
255                    imported_by.push(s);
256                }
257            }
258        }
259
260        Ok(FileDeps {
261            file: file.to_string(),
262            imports,
263            imported_by,
264        })
265    }
266
267    /// Get full type hierarchy for a class/interface: ancestors (up) and descendants (down).
268    pub fn get_type_hierarchy(&self, symbol_id: &str, max_depth: u32) -> Result<TypeHierarchy> {
269        let esc = symbol_id.replace('\'', "\\'");
270
271        // Ancestors: walk INHERITS edges upward
272        let q_up = format!(
273            "MATCH (root:Symbol)-[:INHERITS* 1..{}]->(ancestor:Symbol) \
274             WHERE root.id = '{}' \
275             RETURN ancestor.id, ancestor.name, ancestor.kind, ancestor.file",
276            max_depth, esc
277        );
278        let r = self
279            .conn
280            .query(&q_up)
281            .map_err(|e| anyhow::anyhow!("query failed: {e}"))?;
282        let mut ancestors = Vec::new();
283        for row in r {
284            if row.len() >= 4 {
285                ancestors.push(HierarchyNode {
286                    id: row[0].to_string(),
287                    name: row[1].to_string(),
288                    kind: row[2].to_string(),
289                    file: row[3].to_string(),
290                });
291            }
292        }
293
294        // Descendants: walk INHERITS edges downward
295        let q_down = format!(
296            "MATCH (descendant:Symbol)-[:INHERITS* 1..{}]->(root:Symbol) \
297             WHERE root.id = '{}' \
298             RETURN descendant.id, descendant.name, descendant.kind, descendant.file",
299            max_depth, esc
300        );
301        let r2 = self
302            .conn
303            .query(&q_down)
304            .map_err(|e| anyhow::anyhow!("query failed: {e}"))?;
305        let mut descendants = Vec::new();
306        for row in r2 {
307            if row.len() >= 4 {
308                descendants.push(HierarchyNode {
309                    id: row[0].to_string(),
310                    name: row[1].to_string(),
311                    kind: row[2].to_string(),
312                    file: row[3].to_string(),
313                });
314            }
315        }
316
317        // Also get root symbol info
318        let root_detail = self.find_symbol_by_id(symbol_id)?;
319
320        Ok(TypeHierarchy {
321            root_id: symbol_id.to_string(),
322            root_name: root_detail
323                .as_ref()
324                .map(|s| s.name.clone())
325                .unwrap_or_default(),
326            ancestors,
327            descendants,
328        })
329    }
330
331    /// Get test coverage: which symbols have TESTED_BY edges, which don't.
332    pub fn get_test_coverage(&self) -> Result<TestCoverage> {
333        // Testable kinds
334        let q_covered = "MATCH (s:Symbol)-[:TESTED_BY]->(t:Symbol) \
335                         WHERE s.kind IN ['Function','Method','Class','Struct','Trait','Interface'] \
336                         RETURN DISTINCT s.id, s.name, s.kind, s.file, t.id";
337        let r = self
338            .conn
339            .query(q_covered)
340            .map_err(|e| anyhow::anyhow!("query failed: {e}"))?;
341        let mut covered = Vec::new();
342        for row in r {
343            if row.len() >= 5 {
344                covered.push(CoverageRow {
345                    symbol_id: row[0].to_string(),
346                    symbol_name: row[1].to_string(),
347                    kind: row[2].to_string(),
348                    file: row[3].to_string(),
349                    test_id: Some(row[4].to_string()),
350                });
351            }
352        }
353
354        let q_uncovered = "MATCH (s:Symbol) \
355                           WHERE s.kind IN ['Function','Method','Class','Struct','Trait','Interface'] \
356                           AND NOT EXISTS { MATCH (s)-[:TESTED_BY]->(:Symbol) } \
357                           RETURN s.id, s.name, s.kind, s.file \
358                           ORDER BY s.file, s.start_line";
359        let r2 = self
360            .conn
361            .query(q_uncovered)
362            .map_err(|e| anyhow::anyhow!("query failed: {e}"))?;
363        let mut uncovered = Vec::new();
364        for row in r2 {
365            if row.len() >= 4 {
366                uncovered.push(CoverageRow {
367                    symbol_id: row[0].to_string(),
368                    symbol_name: row[1].to_string(),
369                    kind: row[2].to_string(),
370                    file: row[3].to_string(),
371                    test_id: None,
372                });
373            }
374        }
375
376        let total = covered.len() + uncovered.len();
377        let pct = if total > 0 {
378            (covered.len() * 100) / total
379        } else {
380            0
381        };
382
383        Ok(TestCoverage {
384            covered_count: covered.len(),
385            uncovered_count: uncovered.len(),
386            coverage_pct: pct,
387            covered,
388            uncovered,
389        })
390    }
391
392    /// Run a raw Cypher query and return string results.
393    pub fn raw_query(&self, cypher: &str) -> Result<Vec<Vec<String>>> {
394        let result = self
395            .conn
396            .query(cypher)
397            .map_err(|e| anyhow::anyhow!("query failed: {e}"))?;
398
399        let mut rows = Vec::new();
400        for row in result {
401            let string_row: Vec<String> = row.iter().map(|v| v.to_string()).collect();
402            rows.push(string_row);
403        }
404        Ok(rows)
405    }
406
407    /// Derive TESTED_BY edges from CALLS: if a Test symbol calls a non-test symbol,
408    /// create (called)-[:TESTED_BY]->(test). Returns number of edges created.
409    pub fn derive_tested_by_edges(&self) -> Result<usize> {
410        let _ = self.conn.query("MATCH (s:Symbol)-[r:TESTED_BY]->(t:Symbol) DELETE r");
411        self.conn.query(
412            "MATCH (t:Symbol)-[:CALLS]->(s:Symbol) \
413             WHERE t.kind = 'Test' AND s.kind <> 'Test' \
414             CREATE (s)-[:TESTED_BY]->(t)"
415        ).map_err(|e| anyhow::anyhow!("derive TESTED_BY failed: {e}"))?;
416        let mut r = self.conn.query("MATCH ()-[r:TESTED_BY]->() RETURN count(r)")
417            .map_err(|e| anyhow::anyhow!("count TESTED_BY failed: {e}"))?;
418        let count = r.next()
419            .and_then(|row| row.first().map(|v| v.to_string()))
420            .and_then(|s| s.parse::<usize>().ok())
421            .unwrap_or(0);
422        Ok(count)
423    }
424
425    pub fn generate_test_context(&self, file_filter: Option<&str>, limit: usize) -> Result<TestContext> {
426        let framework = self.detect_test_framework()?;
427        let example_test = self.find_example_test(file_filter)?;
428
429        let q = String::from(
430            "MATCH (s:Symbol) \
431             WHERE s.kind IN ['Function','Method','Class','Struct','Trait','Interface'] \
432             AND NOT EXISTS { MATCH (s)-[:TESTED_BY]->(:Symbol) } \
433             RETURN s.id, s.name, s.kind, s.file, s.start_line, s.end_line, \
434                    s.visibility, s.parameters, s.return_type, s.complexity \
435             ORDER BY s.complexity DESC, s.file, s.start_line"
436        );
437        let mut result = self.conn.query(&q)
438            .map_err(|e| anyhow::anyhow!("generate_test_context query failed: {e}"))?;
439
440        let mut targets = Vec::new();
441        while let Some(row) = result.next() {
442            if row.len() < 10 { continue; }
443            let file = row[3].to_string();
444            if let Some(f) = file_filter {
445                if !file.contains(f) { continue; }
446            }
447            let visibility = row[6].to_string();
448            let complexity: u32 = row[9].to_string().parse().unwrap_or(1);
449            let vis_score: u32 = if visibility == "public" || visibility == "pub" { 10 } else { 0 };
450            let priority_score = complexity * 5 + vis_score;
451
452            targets.push(TestTarget {
453                symbol_id: row[0].to_string(),
454                name: row[1].to_string(),
455                kind: row[2].to_string(),
456                file,
457                start_line: row[4].to_string().parse().unwrap_or(0),
458                end_line: row[5].to_string().parse().unwrap_or(0),
459                visibility,
460                parameters: row[7].to_string(),
461                return_type: row[8].to_string(),
462                complexity,
463                callers: Vec::new(),
464                callees: Vec::new(),
465                branches: Vec::new(),
466                priority_score,
467            });
468        }
469
470        targets.sort_by(|a, b| b.priority_score.cmp(&a.priority_score));
471        targets.truncate(limit);
472
473        for t in &mut targets {
474            t.callers = self.callers_of(&t.symbol_id).unwrap_or_default();
475            t.callees = self.callees_of(&t.symbol_id).unwrap_or_default();
476            t.branches = self.branches_of(&t.symbol_id).unwrap_or_default();
477            t.priority_score += t.callers.len() as u32 * 3;
478        }
479
480        targets.sort_by(|a, b| b.priority_score.cmp(&a.priority_score));
481
482        Ok(TestContext { framework, example_test, targets })
483    }
484
485    fn detect_test_framework(&self) -> Result<String> {
486        let q = "MATCH (s:Symbol) WHERE s.kind = 'Test' RETURN s.docstring LIMIT 20";
487        let mut result = self.conn.query(q)
488            .map_err(|e| anyhow::anyhow!("detect_test_framework failed: {e}"))?;
489
490        let mut frameworks = std::collections::HashMap::new();
491        while let Some(row) = result.next() {
492            let doc = row.first().map(|v| v.to_string()).unwrap_or_default();
493            if doc.contains("#[test]") || doc.contains("#[tokio::test]") || doc.contains("#[rstest]") {
494                *frameworks.entry("rust (cargo test)").or_insert(0u32) += 1;
495            }
496            if doc.contains("@Test") || doc.contains("@ParameterizedTest") {
497                *frameworks.entry("java (junit)").or_insert(0) += 1;
498            }
499            if doc.contains("[Test]") || doc.contains("[Fact]") || doc.contains("[Theory]") {
500                *frameworks.entry("csharp (nunit/xunit)").or_insert(0) += 1;
501            }
502            if doc.contains("[TestMethod]") {
503                *frameworks.entry("csharp (mstest)").or_insert(0) += 1;
504            }
505            if doc.contains("@pytest") || doc.contains("@unittest") {
506                *frameworks.entry("python (pytest)").or_insert(0) += 1;
507            }
508        }
509
510        if let Some((fw, _)) = frameworks.into_iter().max_by_key(|(_, count)| *count) {
511            return Ok(fw.to_string());
512        }
513
514        let q2 = "MATCH (d:Dependency) WHERE d.is_dev = true RETURN d.name LIMIT 100";
515        if let Ok(mut r2) = self.conn.query(q2) {
516            while let Some(row) = r2.next() {
517                let dep = row.first().map(|v| v.to_string()).unwrap_or_default();
518                match dep.as_str() {
519                    "jest" | "vitest" | "mocha" | "ava" | "tap" | "cypress" =>
520                        return Ok(format!("javascript ({})", dep)),
521                    "pytest" => return Ok("python (pytest)".to_string()),
522                    "rspec" | "rspec-core" => return Ok("ruby (rspec)".to_string()),
523                    "minitest" => return Ok("ruby (minitest)".to_string()),
524                    "phpunit/phpunit" => return Ok("php (phpunit)".to_string()),
525                    "flutter_test" => return Ok("dart (flutter_test)".to_string()),
526                    "busted" => return Ok("lua (busted)".to_string()),
527                    "pfunit" => return Ok("fortran (pfunit)".to_string()),
528                    "hspec" | "tasty" | "HUnit" => return Ok(format!("haskell ({})", dep)),
529                    "Test::More" | "Test2" => return Ok(format!("perl ({})", dep)),
530                    _ => {
531                        if dep.contains("kotlin-test") || dep.contains("kotest") {
532                            return Ok(format!("kotlin ({})", dep));
533                        }
534                        if dep.contains("scalatest") || dep.contains("specs2") || dep.contains("munit") {
535                            return Ok(format!("scala ({})", dep));
536                        }
537                    }
538                }
539            }
540        }
541
542        let q3 = "MATCH (s:Symbol) WHERE s.kind = 'Test' \
543                   RETURN s.language, count(s) ORDER BY count(s) DESC LIMIT 1";
544        if let Ok(mut r3) = self.conn.query(q3) {
545            if let Some(row) = r3.next() {
546                let lang = row.first().map(|v| v.to_string()).unwrap_or_default();
547                let fw = match lang.as_str() {
548                    "go" => "go (go test)",
549                    "elixir" => "elixir (ExUnit)",
550                    "swift" => "swift (XCTest)",
551                    "erlang" => "erlang (EUnit/CT)",
552                    "zig" => "zig (builtin test)",
553                    "dart" => "dart (test)",
554                    "julia" => "julia (Test)",
555                    "rust" => "rust (cargo test)",
556                    "python" => "python (unittest/pytest)",
557                    "ruby" => "ruby (minitest/rspec)",
558                    "lua" => "lua (busted)",
559                    "r" => "r (testthat)",
560                    "haskell" => "haskell (hspec/tasty)",
561                    "ocaml" => "ocaml (alcotest/ounit)",
562                    "fortran" => "fortran (pfunit)",
563                    "powershell" => "powershell (pester)",
564                    "bash" => "bash (bats)",
565                    _ if !lang.is_empty() => return Ok(format!("{} (detected)", lang)),
566                    _ => "unknown",
567                };
568                if fw != "unknown" {
569                    return Ok(fw.to_string());
570                }
571            }
572        }
573
574        Ok("unknown".to_string())
575    }
576
577    fn find_example_test(&self, file_filter: Option<&str>) -> Result<Option<ExampleTest>> {
578        let q = if let Some(f) = file_filter {
579            format!(
580                "MATCH (s:Symbol) WHERE s.kind = 'Test' AND s.file CONTAINS '{}' \
581                 RETURN s.id, s.name, s.file, s.start_line, s.end_line LIMIT 1",
582                f.replace('\'', "\\'")
583            )
584        } else {
585            "MATCH (s:Symbol) WHERE s.kind = 'Test' \
586             RETURN s.id, s.name, s.file, s.start_line, s.end_line LIMIT 1".to_string()
587        };
588
589        let mut result = self.conn.query(&q)
590            .map_err(|e| anyhow::anyhow!("find_example_test failed: {e}"))?;
591
592        if let Some(row) = result.next() {
593            if row.len() >= 5 {
594                return Ok(Some(ExampleTest {
595                    symbol_id: row[0].to_string(),
596                    name: row[1].to_string(),
597                    file: row[2].to_string(),
598                    start_line: row[3].to_string().parse().unwrap_or(0),
599                    end_line: row[4].to_string().parse().unwrap_or(0),
600                }));
601            }
602        }
603        Ok(None)
604    }
605
606    fn collect_strings(&self, query: &str) -> Result<Vec<String>> {
607        let result = self
608            .conn
609            .query(query)
610            .map_err(|e| anyhow::anyhow!("query failed: {e}"))?;
611        let mut out = Vec::new();
612        for row in result {
613            if let Some(val) = row.first() {
614                out.push(val.to_string());
615            }
616        }
617        Ok(out)
618    }
619}
620
621#[derive(Debug, Serialize)]
622pub struct SymbolRow {
623    pub id: String,
624    pub name: String,
625    pub kind: String,
626    pub start_line: u32,
627    pub end_line: u32,
628}
629
630/// Extended symbol info including file path (for snippet retrieval).
631#[derive(Debug, Serialize)]
632pub struct SymbolDetail {
633    pub id: String,
634    pub name: String,
635    pub kind: String,
636    pub file: String,
637    pub start_line: u32,
638    pub end_line: u32,
639}
640
641#[derive(Debug, Serialize)]
642pub struct ImpactRow {
643    pub id: String,
644    pub name: String,
645    pub file: String,
646    pub kind: String,
647}
648
649#[derive(Debug, Serialize)]
650pub struct ReferenceRow {
651    pub caller_id: String,
652    pub caller_name: String,
653    pub file: String,
654    pub line: u32,
655    pub target_id: String,
656}
657
658#[derive(Debug, Serialize)]
659pub struct ApiSymbol {
660    pub id: String,
661    pub name: String,
662    pub kind: String,
663    pub file: String,
664    pub line: u32,
665    pub visibility: String,
666    pub docstring: String,
667}
668
669#[derive(Debug, Serialize)]
670pub struct FileDeps {
671    pub file: String,
672    pub imports: Vec<String>,
673    pub imported_by: Vec<String>,
674}
675
676#[derive(Debug, Serialize)]
677pub struct HierarchyNode {
678    pub id: String,
679    pub name: String,
680    pub kind: String,
681    pub file: String,
682}
683
684#[derive(Debug, Serialize)]
685pub struct TypeHierarchy {
686    pub root_id: String,
687    pub root_name: String,
688    pub ancestors: Vec<HierarchyNode>,
689    pub descendants: Vec<HierarchyNode>,
690}
691
692#[derive(Debug, Serialize)]
693pub struct CoverageRow {
694    pub symbol_id: String,
695    pub symbol_name: String,
696    pub kind: String,
697    pub file: String,
698    pub test_id: Option<String>,
699}
700
701#[derive(Debug, Serialize)]
702pub struct TestCoverage {
703    pub covered_count: usize,
704    pub uncovered_count: usize,
705    pub coverage_pct: usize,
706    pub covered: Vec<CoverageRow>,
707    pub uncovered: Vec<CoverageRow>,
708}
709
710#[derive(Debug, Serialize)]
711pub struct BranchInfo {
712    pub kind: String,
713    pub condition: String,
714    pub line: u32,
715    pub depth: u32,
716}
717
718#[derive(Debug, Serialize)]
719pub struct TestTarget {
720    pub symbol_id: String,
721    pub name: String,
722    pub kind: String,
723    pub file: String,
724    pub start_line: u32,
725    pub end_line: u32,
726    pub visibility: String,
727    pub parameters: String,
728    pub return_type: String,
729    pub complexity: u32,
730    pub callers: Vec<String>,
731    pub callees: Vec<String>,
732    pub branches: Vec<BranchInfo>,
733    pub priority_score: u32,
734}
735
736#[derive(Debug, Serialize)]
737pub struct TestContext {
738    pub framework: String,
739    pub example_test: Option<ExampleTest>,
740    pub targets: Vec<TestTarget>,
741}
742
743#[derive(Debug, Serialize)]
744pub struct ExampleTest {
745    pub symbol_id: String,
746    pub name: String,
747    pub file: String,
748    pub start_line: u32,
749    pub end_line: u32,
750}