1use anyhow::Result;
2use kuzu::Connection;
3use serde::Serialize;
4
5pub 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 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 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 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 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 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 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 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 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 pub fn get_file_deps(&self, file: &str) -> Result<FileDeps> {
220 let esc = file.replace('\'', "\\'");
221
222 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 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 pub fn get_type_hierarchy(&self, symbol_id: &str, max_depth: u32) -> Result<TypeHierarchy> {
269 let esc = symbol_id.replace('\'', "\\'");
270
271 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 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 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 pub fn get_test_coverage(&self) -> Result<TestCoverage> {
333 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 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 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#[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}