1use crate::schema::{
10 CodeEdgePredicate, CodeNode, CodeNodeKind, edge_col, extract_file_path, node_col,
11};
12use arrow::array::{Array, Float32Array, Float64Array, Int32Array, RecordBatch, StringArray};
13use std::collections::{HashMap, HashSet, VecDeque};
14
15#[derive(Debug, Default, Clone)]
17pub struct CodeSearch {
18 pub kind: Option<CodeNodeKind>,
20 pub name_pattern: Option<String>,
22 pub signature_pattern: Option<String>,
24 pub docstring_pattern: Option<String>,
26 pub body_pattern: Option<String>,
28 pub file_prefix: Option<String>,
30 pub min_loc: Option<i32>,
32 pub min_complexity: Option<i32>,
34 pub max_coverage: Option<f64>,
36 pub limit: Option<usize>,
38}
39
40#[derive(Debug)]
42pub struct SearchResult {
43 pub nodes: Vec<CodeNode>,
45 pub total_scanned: usize,
47}
48
49pub fn search_nodes(batch: &RecordBatch, query: &CodeSearch) -> SearchResult {
54 if batch.num_rows() == 0 {
55 return SearchResult {
56 nodes: Vec::new(),
57 total_scanned: 0,
58 };
59 }
60
61 let ids = col_str(batch, node_col::ID);
62 let names = col_str(batch, node_col::NAME);
63 let parent_ids = col_str(batch, node_col::PARENT_ID);
64 let signatures = col_str(batch, node_col::SIGNATURE);
65 let docstrings = col_str(batch, node_col::DOCSTRING);
66 let body_hashes = col_str(batch, node_col::BODY_HASH);
67 let bodies = batch
68 .column(node_col::BODY)
69 .as_any()
70 .downcast_ref::<arrow::array::LargeStringArray>()
71 .expect("body column");
72 let locs = col_i32(batch, node_col::LOC);
73 let complexities = col_i32(batch, node_col::CYCLOMATIC_COMPLEXITY);
74 let coverages = col_f64(batch, node_col::COVERAGE_PCT);
75 let (kind_dict, kind_values) = col_dict_str(batch, node_col::KIND);
76
77 let total_scanned = batch.num_rows();
78 let mut matched = Vec::new();
79
80 for i in 0..total_scanned {
81 let kind_key = kind_dict.keys().value(i) as usize;
82 let kind_str = kind_values.value(kind_key);
83 let name = names.value(i);
84 let id = ids.value(i);
85
86 if let Some(ref k) = query.kind
88 && kind_str != k.as_str()
89 {
90 continue;
91 }
92
93 if let Some(ref pat) = query.name_pattern
95 && !name.to_lowercase().contains(&pat.to_lowercase())
96 {
97 continue;
98 }
99
100 if let Some(ref pat) = query.signature_pattern
102 && (signatures.is_null(i)
103 || !signatures
104 .value(i)
105 .to_lowercase()
106 .contains(&pat.to_lowercase()))
107 {
108 continue;
109 }
110
111 if let Some(ref pat) = query.docstring_pattern
113 && (docstrings.is_null(i)
114 || !docstrings
115 .value(i)
116 .to_lowercase()
117 .contains(&pat.to_lowercase()))
118 {
119 continue;
120 }
121
122 if let Some(ref pat) = query.body_pattern
124 && (bodies.is_null(i) || !bodies.value(i).to_lowercase().contains(&pat.to_lowercase()))
125 {
126 continue;
127 }
128
129 if let Some(ref prefix) = query.file_prefix {
131 let file_path = extract_file_path(id);
132 match file_path {
133 Some(fp) if fp.starts_with(prefix.as_str()) => {}
134 _ => continue,
135 }
136 }
137
138 if let Some(min) = query.min_loc
140 && (locs.is_null(i) || locs.value(i) < min)
141 {
142 continue;
143 }
144 if let Some(min) = query.min_complexity
145 && (complexities.is_null(i) || complexities.value(i) < min)
146 {
147 continue;
148 }
149 if let Some(max) = query.max_coverage
150 && (coverages.is_null(i) || coverages.value(i) > max)
151 {
152 continue;
153 }
154
155 let node = extract_node(
156 i,
157 ids,
158 kind_str,
159 parent_ids,
160 names,
161 signatures,
162 docstrings,
163 body_hashes,
164 locs,
165 complexities,
166 coverages,
167 );
168 matched.push(node);
169
170 if let Some(limit) = query.limit
171 && matched.len() >= limit
172 {
173 break;
174 }
175 }
176
177 SearchResult {
178 nodes: matched,
179 total_scanned,
180 }
181}
182
183pub fn find_sources(
187 target_id: &str,
188 predicate: CodeEdgePredicate,
189 nodes_batch: &RecordBatch,
190 edges_batch: &RecordBatch,
191) -> Vec<CodeNode> {
192 let source_ids =
193 collect_edge_endpoints(edges_batch, target_id, predicate, EdgeDirection::Incoming);
194 let refs: Vec<&str> = source_ids.iter().map(|s| s.as_str()).collect();
195 resolve_node_ids(&refs, nodes_batch)
196}
197
198pub fn find_targets(
202 source_id: &str,
203 predicate: CodeEdgePredicate,
204 nodes_batch: &RecordBatch,
205 edges_batch: &RecordBatch,
206) -> Vec<CodeNode> {
207 let target_ids =
208 collect_edge_endpoints(edges_batch, source_id, predicate, EdgeDirection::Outgoing);
209 let refs: Vec<&str> = target_ids.iter().map(|s| s.as_str()).collect();
210 resolve_node_ids(&refs, nodes_batch)
211}
212
213pub fn callers(
215 node_id: &str,
216 nodes_batch: &RecordBatch,
217 edges_batch: &RecordBatch,
218) -> Vec<CodeNode> {
219 find_sources(node_id, CodeEdgePredicate::Calls, nodes_batch, edges_batch)
220}
221
222pub fn callees(
224 node_id: &str,
225 nodes_batch: &RecordBatch,
226 edges_batch: &RecordBatch,
227) -> Vec<CodeNode> {
228 find_targets(node_id, CodeEdgePredicate::Calls, nodes_batch, edges_batch)
229}
230
231pub fn tests_for(
233 node_id: &str,
234 nodes_batch: &RecordBatch,
235 edges_batch: &RecordBatch,
236) -> Vec<CodeNode> {
237 find_sources(node_id, CodeEdgePredicate::Tests, nodes_batch, edges_batch)
238}
239
240pub fn children_of(
242 node_id: &str,
243 nodes_batch: &RecordBatch,
244 edges_batch: &RecordBatch,
245) -> Vec<CodeNode> {
246 find_targets(
247 node_id,
248 CodeEdgePredicate::Contains,
249 nodes_batch,
250 edges_batch,
251 )
252}
253
254pub fn transitive_deps(
259 start_id: &str,
260 predicate: CodeEdgePredicate,
261 max_depth: usize,
262 nodes_batch: &RecordBatch,
263 edges_batch: &RecordBatch,
264) -> Vec<CodeNode> {
265 let adj = build_adjacency(edges_batch, predicate, EdgeDirection::Outgoing);
267
268 let mut visited: HashSet<String> = HashSet::new();
270 visited.insert(start_id.to_string());
271 let mut queue: VecDeque<(String, usize)> = VecDeque::new();
272 queue.push_back((start_id.to_string(), 0));
273 let mut reachable: Vec<String> = Vec::new();
274
275 while let Some((current, depth)) = queue.pop_front() {
276 if depth >= max_depth {
277 continue;
278 }
279 if let Some(neighbors) = adj.get(¤t) {
280 for neighbor in neighbors {
281 if visited.insert(neighbor.clone()) {
282 reachable.push(neighbor.clone());
283 queue.push_back((neighbor.clone(), depth + 1));
284 }
285 }
286 }
287 }
288
289 resolve_node_ids(
290 &reachable.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
291 nodes_batch,
292 )
293}
294
295pub fn transitive_callers(
301 target_id: &str,
302 predicate: CodeEdgePredicate,
303 max_depth: usize,
304 nodes_batch: &RecordBatch,
305 edges_batch: &RecordBatch,
306) -> Vec<CodeNode> {
307 let adj = build_adjacency(edges_batch, predicate, EdgeDirection::Incoming);
308
309 let mut visited: HashSet<String> = HashSet::new();
310 visited.insert(target_id.to_string());
311 let mut queue: VecDeque<(String, usize)> = VecDeque::new();
312 queue.push_back((target_id.to_string(), 0));
313 let mut reachable: Vec<String> = Vec::new();
314
315 while let Some((current, depth)) = queue.pop_front() {
316 if depth >= max_depth {
317 continue;
318 }
319 if let Some(neighbors) = adj.get(¤t) {
320 for neighbor in neighbors {
321 if visited.insert(neighbor.clone()) {
322 reachable.push(neighbor.clone());
323 queue.push_back((neighbor.clone(), depth + 1));
324 }
325 }
326 }
327 }
328
329 resolve_node_ids(
330 &reachable.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
331 nodes_batch,
332 )
333}
334
335#[derive(Clone, Copy)]
338enum EdgeDirection {
339 Incoming,
341 Outgoing,
343}
344
345fn collect_edge_endpoints(
347 edges_batch: &RecordBatch,
348 node_id: &str,
349 predicate: CodeEdgePredicate,
350 direction: EdgeDirection,
351) -> Vec<String> {
352 if edges_batch.num_rows() == 0 {
353 return Vec::new();
354 }
355
356 let sources = col_str(edges_batch, edge_col::SOURCE_ID);
357 let targets = col_str(edges_batch, edge_col::TARGET_ID);
358 let (pred_dict, pred_values) = col_dict_str(edges_batch, edge_col::PREDICATE);
359 let weights = edges_batch
360 .column(edge_col::WEIGHT)
361 .as_any()
362 .downcast_ref::<Float32Array>()
363 .expect("weight");
364
365 let pred_str = predicate.as_str();
366 let mut results = Vec::new();
367
368 for i in 0..edges_batch.num_rows() {
369 if !weights.is_null(i) && weights.value(i) < 0.0 {
371 continue;
372 }
373
374 let pred_key = pred_dict.keys().value(i) as usize;
375 if pred_values.value(pred_key) != pred_str {
376 continue;
377 }
378
379 match direction {
380 EdgeDirection::Incoming => {
381 if targets.value(i) == node_id {
382 results.push(sources.value(i).to_string());
383 }
384 }
385 EdgeDirection::Outgoing => {
386 if sources.value(i) == node_id {
387 results.push(targets.value(i).to_string());
388 }
389 }
390 }
391 }
392
393 results
394}
395
396fn build_adjacency(
398 edges_batch: &RecordBatch,
399 predicate: CodeEdgePredicate,
400 direction: EdgeDirection,
401) -> HashMap<String, Vec<String>> {
402 let mut adj: HashMap<String, Vec<String>> = HashMap::new();
403
404 if edges_batch.num_rows() == 0 {
405 return adj;
406 }
407
408 let sources = col_str(edges_batch, edge_col::SOURCE_ID);
409 let targets = col_str(edges_batch, edge_col::TARGET_ID);
410 let (pred_dict, pred_values) = col_dict_str(edges_batch, edge_col::PREDICATE);
411 let weights = edges_batch
412 .column(edge_col::WEIGHT)
413 .as_any()
414 .downcast_ref::<Float32Array>()
415 .expect("weight");
416
417 let pred_str = predicate.as_str();
418
419 for i in 0..edges_batch.num_rows() {
420 if !weights.is_null(i) && weights.value(i) < 0.0 {
421 continue;
422 }
423
424 let pred_key = pred_dict.keys().value(i) as usize;
425 if pred_values.value(pred_key) != pred_str {
426 continue;
427 }
428
429 match direction {
430 EdgeDirection::Outgoing => {
431 adj.entry(sources.value(i).to_string())
432 .or_default()
433 .push(targets.value(i).to_string());
434 }
435 EdgeDirection::Incoming => {
436 adj.entry(targets.value(i).to_string())
437 .or_default()
438 .push(sources.value(i).to_string());
439 }
440 }
441 }
442
443 adj
444}
445
446fn resolve_node_ids(ids: &[&str], batch: &RecordBatch) -> Vec<CodeNode> {
448 if batch.num_rows() == 0 || ids.is_empty() {
449 return Vec::new();
450 }
451
452 let id_set: HashSet<&str> = ids.iter().copied().collect();
453 let batch_ids = col_str(batch, node_col::ID);
454 let names = col_str(batch, node_col::NAME);
455 let parent_ids = col_str(batch, node_col::PARENT_ID);
456 let signatures = col_str(batch, node_col::SIGNATURE);
457 let docstrings = col_str(batch, node_col::DOCSTRING);
458 let body_hashes = col_str(batch, node_col::BODY_HASH);
459 let locs = col_i32(batch, node_col::LOC);
460 let complexities = col_i32(batch, node_col::CYCLOMATIC_COMPLEXITY);
461 let coverages = col_f64(batch, node_col::COVERAGE_PCT);
462 let (kind_dict, kind_values) = col_dict_str(batch, node_col::KIND);
463
464 let mut result = Vec::new();
465 for i in 0..batch.num_rows() {
466 let id = batch_ids.value(i);
467 if id_set.contains(id) {
468 let kind_key = kind_dict.keys().value(i) as usize;
469 let kind_str = kind_values.value(kind_key);
470 result.push(extract_node(
471 i,
472 batch_ids,
473 kind_str,
474 parent_ids,
475 names,
476 signatures,
477 docstrings,
478 body_hashes,
479 locs,
480 complexities,
481 coverages,
482 ));
483 }
484 }
485 result
486}
487
488fn col_str(batch: &RecordBatch, idx: usize) -> &StringArray {
491 batch
492 .column(idx)
493 .as_any()
494 .downcast_ref::<StringArray>()
495 .expect("string column")
496}
497
498fn col_i32(batch: &RecordBatch, idx: usize) -> &Int32Array {
499 batch
500 .column(idx)
501 .as_any()
502 .downcast_ref::<Int32Array>()
503 .expect("int32 column")
504}
505
506fn col_f64(batch: &RecordBatch, idx: usize) -> &Float64Array {
507 batch
508 .column(idx)
509 .as_any()
510 .downcast_ref::<Float64Array>()
511 .expect("float64 column")
512}
513
514fn col_dict_str(
515 batch: &RecordBatch,
516 idx: usize,
517) -> (&arrow::array::Int8DictionaryArray, &StringArray) {
518 let dict = batch
519 .column(idx)
520 .as_any()
521 .downcast_ref::<arrow::array::Int8DictionaryArray>()
522 .expect("dict column");
523 let values = dict
524 .values()
525 .as_any()
526 .downcast_ref::<StringArray>()
527 .expect("dict values");
528 (dict, values)
529}
530
531#[allow(clippy::too_many_arguments)]
532fn extract_node(
533 i: usize,
534 ids: &StringArray,
535 kind_str: &str,
536 parent_ids: &StringArray,
537 names: &StringArray,
538 signatures: &StringArray,
539 docstrings: &StringArray,
540 body_hashes: &StringArray,
541 locs: &Int32Array,
542 complexities: &Int32Array,
543 coverages: &Float64Array,
544) -> CodeNode {
545 CodeNode {
546 id: ids.value(i).to_string(),
547 kind: CodeNodeKind::parse(kind_str).unwrap_or(CodeNodeKind::Function),
548 parent_id: if parent_ids.is_null(i) {
549 None
550 } else {
551 Some(parent_ids.value(i).to_string())
552 },
553 name: names.value(i).to_string(),
554 signature: if signatures.is_null(i) {
555 None
556 } else {
557 Some(signatures.value(i).to_string())
558 },
559 docstring: if docstrings.is_null(i) {
560 None
561 } else {
562 Some(docstrings.value(i).to_string())
563 },
564 body_hash: if body_hashes.is_null(i) {
565 None
566 } else {
567 Some(body_hashes.value(i).to_string())
568 },
569 body: None, loc: if locs.is_null(i) {
571 None
572 } else {
573 Some(locs.value(i))
574 },
575 cyclomatic_complexity: if complexities.is_null(i) {
576 None
577 } else {
578 Some(complexities.value(i))
579 },
580 coverage_pct: if coverages.is_null(i) {
581 None
582 } else {
583 Some(coverages.value(i))
584 },
585 last_modified: None,
586 ..Default::default()
587 }
588}
589
590#[cfg(test)]
591mod tests {
592 use super::*;
593 use crate::schema::{
594 CodeEdge, CodeEdgePredicate, CodeNode, CodeNodeKind, build_code_edges_batch,
595 build_code_nodes_batch,
596 };
597
598 fn sample_nodes() -> Vec<CodeNode> {
599 vec![
600 CodeNode {
601 id: "mod:brain/signal.py".into(),
602 kind: CodeNodeKind::Module,
603 parent_id: None,
604 name: "signal".into(),
605 signature: None,
606 docstring: Some("Signal processing module.".into()),
607 body_hash: Some("mod_h".into()),
608 body: None,
609 loc: Some(200),
610 cyclomatic_complexity: None,
611 coverage_pct: None,
612 last_modified: None,
613 ..Default::default()
614 },
615 CodeNode {
616 id: "func:brain/signal.py::fuse".into(),
617 kind: CodeNodeKind::Function,
618 parent_id: Some("mod:brain/signal.py".into()),
619 name: "fuse".into(),
620 signature: Some("def fuse(signals: List) -> Decision".into()),
621 docstring: Some("Fuse cognitive signals.".into()),
622 body_hash: Some("fuse_h".into()),
623 body: None,
624 loc: Some(42),
625 cyclomatic_complexity: Some(8),
626 coverage_pct: Some(0.85),
627 last_modified: None,
628 ..Default::default()
629 },
630 CodeNode {
631 id: "func:brain/signal.py::validate".into(),
632 kind: CodeNodeKind::Function,
633 parent_id: Some("mod:brain/signal.py".into()),
634 name: "validate".into(),
635 signature: Some("def validate(sig: Signal) -> bool".into()),
636 docstring: None,
637 body_hash: Some("val_h".into()),
638 body: None,
639 loc: Some(15),
640 cyclomatic_complexity: Some(2),
641 coverage_pct: Some(0.95),
642 last_modified: None,
643 ..Default::default()
644 },
645 CodeNode {
646 id: "func:brain/store.py::promote".into(),
647 kind: CodeNodeKind::Function,
648 parent_id: None,
649 name: "promote".into(),
650 signature: Some("def promote(item) -> None".into()),
651 docstring: Some("Promote to long-term.".into()),
652 body_hash: Some("prom_h".into()),
653 body: None,
654 loc: Some(30),
655 cyclomatic_complexity: Some(4),
656 coverage_pct: Some(0.60),
657 last_modified: None,
658 ..Default::default()
659 },
660 CodeNode {
661 id: "test:brain/signal.py::test_fuse".into(),
662 kind: CodeNodeKind::Test,
663 parent_id: None,
664 name: "test_fuse".into(),
665 signature: None,
666 docstring: None,
667 body_hash: Some("test_h".into()),
668 body: None,
669 loc: Some(10),
670 cyclomatic_complexity: Some(1),
671 coverage_pct: None,
672 last_modified: None,
673 ..Default::default()
674 },
675 CodeNode {
676 id: "func:brain/nn.py::activate".into(),
677 kind: CodeNodeKind::Function,
678 parent_id: None,
679 name: "activate".into(),
680 signature: Some("def activate(x: Tensor) -> Tensor".into()),
681 docstring: Some("Apply activation function.".into()),
682 body_hash: Some("act_h".into()),
683 body: Some("def activate(x):\n return softmax(x)".into()),
684 loc: Some(5),
685 cyclomatic_complexity: Some(1),
686 coverage_pct: Some(0.90),
687 last_modified: None,
688 ..Default::default()
689 },
690 ]
691 }
692
693 fn sample_edges() -> Vec<CodeEdge> {
694 vec![
695 CodeEdge {
697 source_id: "func:brain/signal.py::fuse".into(),
698 target_id: "func:brain/signal.py::validate".into(),
699 predicate: CodeEdgePredicate::Calls,
700 weight: Some(2.0),
701 commit_id: None,
702 },
703 CodeEdge {
705 source_id: "func:brain/signal.py::fuse".into(),
706 target_id: "func:brain/store.py::promote".into(),
707 predicate: CodeEdgePredicate::Calls,
708 weight: Some(1.0),
709 commit_id: None,
710 },
711 CodeEdge {
713 source_id: "func:brain/signal.py::validate".into(),
714 target_id: "func:brain/store.py::promote".into(),
715 predicate: CodeEdgePredicate::Calls,
716 weight: Some(1.0),
717 commit_id: None,
718 },
719 CodeEdge {
721 source_id: "test:brain/signal.py::test_fuse".into(),
722 target_id: "func:brain/signal.py::fuse".into(),
723 predicate: CodeEdgePredicate::Tests,
724 weight: Some(1.0),
725 commit_id: None,
726 },
727 CodeEdge {
729 source_id: "mod:brain/signal.py".into(),
730 target_id: "func:brain/signal.py::fuse".into(),
731 predicate: CodeEdgePredicate::Contains,
732 weight: None,
733 commit_id: None,
734 },
735 CodeEdge {
737 source_id: "mod:brain/signal.py".into(),
738 target_id: "func:brain/signal.py::validate".into(),
739 predicate: CodeEdgePredicate::Contains,
740 weight: None,
741 commit_id: None,
742 },
743 ]
744 }
745
746 #[test]
749 fn test_search_all() {
750 let nodes = build_code_nodes_batch(&sample_nodes()).unwrap();
751 let result = search_nodes(&nodes, &CodeSearch::default());
752 assert_eq!(result.total_scanned, 6);
753 assert_eq!(result.nodes.len(), 6);
754 }
755
756 #[test]
757 fn test_search_by_kind() {
758 let nodes = build_code_nodes_batch(&sample_nodes()).unwrap();
759 let result = search_nodes(
760 &nodes,
761 &CodeSearch {
762 kind: Some(CodeNodeKind::Function),
763 ..Default::default()
764 },
765 );
766 assert_eq!(result.nodes.len(), 4); }
768
769 #[test]
770 fn test_search_by_name_pattern() {
771 let nodes = build_code_nodes_batch(&sample_nodes()).unwrap();
772 let result = search_nodes(
773 &nodes,
774 &CodeSearch {
775 name_pattern: Some("fus".into()),
776 ..Default::default()
777 },
778 );
779 assert_eq!(result.nodes.len(), 2); }
781
782 #[test]
783 fn test_search_by_signature_pattern() {
784 let nodes = build_code_nodes_batch(&sample_nodes()).unwrap();
785 let result = search_nodes(
786 &nodes,
787 &CodeSearch {
788 signature_pattern: Some("-> bool".into()),
789 ..Default::default()
790 },
791 );
792 assert_eq!(result.nodes.len(), 1); assert_eq!(result.nodes[0].name, "validate");
794 }
795
796 #[test]
797 fn test_search_by_docstring_pattern() {
798 let nodes = build_code_nodes_batch(&sample_nodes()).unwrap();
799 let result = search_nodes(
800 &nodes,
801 &CodeSearch {
802 docstring_pattern: Some("cognitive".into()),
803 ..Default::default()
804 },
805 );
806 assert_eq!(result.nodes.len(), 1);
807 assert_eq!(result.nodes[0].name, "fuse");
808 }
809
810 #[test]
811 fn test_search_by_body_pattern() {
812 let nodes = build_code_nodes_batch(&sample_nodes()).unwrap();
813 let result = search_nodes(
815 &nodes,
816 &CodeSearch {
817 body_pattern: Some("softmax".into()),
818 ..Default::default()
819 },
820 );
821 assert_eq!(result.nodes.len(), 1);
822 assert_eq!(result.nodes[0].name, "activate");
823
824 let result = search_nodes(
826 &nodes,
827 &CodeSearch {
828 body_pattern: Some("nonexistent".into()),
829 ..Default::default()
830 },
831 );
832 assert!(result.nodes.is_empty());
833
834 let result = search_nodes(
836 &nodes,
837 &CodeSearch {
838 body_pattern: Some("fuse".into()),
839 ..Default::default()
840 },
841 );
842 assert!(result.nodes.is_empty());
844 }
845
846 #[test]
847 fn test_search_by_file_prefix() {
848 let nodes = build_code_nodes_batch(&sample_nodes()).unwrap();
849 let result = search_nodes(
850 &nodes,
851 &CodeSearch {
852 file_prefix: Some("brain/store".into()),
853 ..Default::default()
854 },
855 );
856 assert_eq!(result.nodes.len(), 1);
857 assert_eq!(result.nodes[0].name, "promote");
858 }
859
860 #[test]
861 fn test_search_combined() {
862 let nodes = build_code_nodes_batch(&sample_nodes()).unwrap();
863 let result = search_nodes(
864 &nodes,
865 &CodeSearch {
866 kind: Some(CodeNodeKind::Function),
867 min_complexity: Some(5),
868 ..Default::default()
869 },
870 );
871 assert_eq!(result.nodes.len(), 1); assert_eq!(result.nodes[0].name, "fuse");
873 }
874
875 #[test]
876 fn test_search_with_limit() {
877 let nodes = build_code_nodes_batch(&sample_nodes()).unwrap();
878 let result = search_nodes(
879 &nodes,
880 &CodeSearch {
881 limit: Some(2),
882 ..Default::default()
883 },
884 );
885 assert_eq!(result.nodes.len(), 2);
886 }
887
888 #[test]
889 fn test_search_empty_batch() {
890 let nodes = build_code_nodes_batch(&[]).unwrap();
891 let result = search_nodes(&nodes, &CodeSearch::default());
892 assert_eq!(result.nodes.len(), 0);
893 assert_eq!(result.total_scanned, 0);
894 }
895
896 #[test]
899 fn test_callers_of_validate() {
900 let nodes = build_code_nodes_batch(&sample_nodes()).unwrap();
901 let edges = build_code_edges_batch(&sample_edges()).unwrap();
902
903 let result = callers("func:brain/signal.py::validate", &nodes, &edges);
904 assert_eq!(result.len(), 1);
905 assert_eq!(result[0].name, "fuse");
906 }
907
908 #[test]
909 fn test_callees_of_fuse() {
910 let nodes = build_code_nodes_batch(&sample_nodes()).unwrap();
911 let edges = build_code_edges_batch(&sample_edges()).unwrap();
912
913 let result = callees("func:brain/signal.py::fuse", &nodes, &edges);
914 assert_eq!(result.len(), 2); let names: HashSet<&str> = result.iter().map(|n| n.name.as_str()).collect();
916 assert!(names.contains("validate"));
917 assert!(names.contains("promote"));
918 }
919
920 #[test]
921 fn test_tests_for_fuse() {
922 let nodes = build_code_nodes_batch(&sample_nodes()).unwrap();
923 let edges = build_code_edges_batch(&sample_edges()).unwrap();
924
925 let result = tests_for("func:brain/signal.py::fuse", &nodes, &edges);
926 assert_eq!(result.len(), 1);
927 assert_eq!(result[0].name, "test_fuse");
928 }
929
930 #[test]
931 fn test_children_of_module() {
932 let nodes = build_code_nodes_batch(&sample_nodes()).unwrap();
933 let edges = build_code_edges_batch(&sample_edges()).unwrap();
934
935 let result = children_of("mod:brain/signal.py", &nodes, &edges);
936 assert_eq!(result.len(), 2); }
938
939 #[test]
940 fn test_callers_of_promote() {
941 let nodes = build_code_nodes_batch(&sample_nodes()).unwrap();
942 let edges = build_code_edges_batch(&sample_edges()).unwrap();
943
944 let result = callers("func:brain/store.py::promote", &nodes, &edges);
945 assert_eq!(result.len(), 2); }
947
948 #[test]
949 fn test_no_callers() {
950 let nodes = build_code_nodes_batch(&sample_nodes()).unwrap();
951 let edges = build_code_edges_batch(&sample_edges()).unwrap();
952
953 let result = callers("func:brain/signal.py::fuse", &nodes, &edges);
954 assert!(result.is_empty());
956 }
957
958 #[test]
961 fn test_transitive_callees_depth_1() {
962 let nodes = build_code_nodes_batch(&sample_nodes()).unwrap();
963 let edges = build_code_edges_batch(&sample_edges()).unwrap();
964
965 let result = transitive_deps(
966 "func:brain/signal.py::fuse",
967 CodeEdgePredicate::Calls,
968 1,
969 &nodes,
970 &edges,
971 );
972 assert_eq!(result.len(), 2);
974 }
975
976 #[test]
977 fn test_transitive_callees_depth_2() {
978 let nodes = build_code_nodes_batch(&sample_nodes()).unwrap();
979 let edges = build_code_edges_batch(&sample_edges()).unwrap();
980
981 let result = transitive_deps(
982 "func:brain/signal.py::fuse",
983 CodeEdgePredicate::Calls,
984 2,
985 &nodes,
986 &edges,
987 );
988 assert_eq!(result.len(), 2); }
991
992 #[test]
993 fn test_transitive_callers() {
994 let nodes = build_code_nodes_batch(&sample_nodes()).unwrap();
995 let edges = build_code_edges_batch(&sample_edges()).unwrap();
996
997 let result = transitive_callers(
998 "func:brain/store.py::promote",
999 CodeEdgePredicate::Calls,
1000 2,
1001 &nodes,
1002 &edges,
1003 );
1004 assert_eq!(result.len(), 2); }
1007
1008 #[test]
1009 fn test_transitive_depth_0_returns_nothing() {
1010 let nodes = build_code_nodes_batch(&sample_nodes()).unwrap();
1011 let edges = build_code_edges_batch(&sample_edges()).unwrap();
1012
1013 let result = transitive_deps(
1014 "func:brain/signal.py::fuse",
1015 CodeEdgePredicate::Calls,
1016 0,
1017 &nodes,
1018 &edges,
1019 );
1020 assert!(result.is_empty());
1021 }
1022
1023 #[test]
1024 fn test_find_sources_empty_edges() {
1025 let nodes = build_code_nodes_batch(&sample_nodes()).unwrap();
1026 let edges = build_code_edges_batch(&[]).unwrap();
1027
1028 let result = find_sources(
1029 "func:brain/signal.py::fuse",
1030 CodeEdgePredicate::Calls,
1031 &nodes,
1032 &edges,
1033 );
1034 assert!(result.is_empty());
1035 }
1036
1037 #[test]
1038 fn test_extract_file_path() {
1039 assert_eq!(
1040 extract_file_path("func:brain/signal.py::fuse"),
1041 Some("brain/signal.py".to_string())
1042 );
1043 assert_eq!(
1044 extract_file_path("mod:brain/signal.py"),
1045 Some("brain/signal.py".to_string())
1046 );
1047 assert_eq!(extract_file_path("no-prefix"), None);
1048 }
1049}