basemind 0.24.0

Full AI context layer over MCP — tree-sitter code-map, document RAG (PDF/Office/HTML/email + OCR + reranker), shared agent memory, on-demand web crawl, git history + blame + per-symbol diff. 300+ languages, 10+ coding-agent harnesses, content-addressed Fjall + LanceDB.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
//! Bounded, deterministic traversal over the shared code-graph (ADR-0003).
//!
//! Three shapes read the [`CodeGraph`](super::codegraph::CodeGraph) built by
//! [`codegraph::build`](super::codegraph::build):
//!
//! - [`neighbors`] — N-hop expansion outward from a set of root nodes, filtered by edge
//!   kind, direction, and a minimum-confidence floor.
//! - [`shortest_path`] — a confidence-weighted shortest path between two symbol sets, so a
//!   proven (EXTRACTED) edge is preferred over an inferred one of equal hop length.
//! - [`subgraph`] — the neighborhood around a root, cut to the significant head by local
//!   centrality so the result is a readable subgraph rather than a dump.
//!
//! This module is pure graph machinery: it operates on interned node ids over a prebuilt
//! [`Adjacency`] and never touches the store or the L1 cache — resolving names to nodes and
//! describing nodes for the response is the caller's job (see `helpers_traverse`). Edge cost
//! is an integer on the provenance ladder ([`edge_cost`]) so ordering is exact and
//! deterministic — no floating-point comparison in the hot path.

use std::cmp::Reverse;
use std::collections::{BinaryHeap, VecDeque};
use std::sync::Arc;

use ahash::{AHashMap, AHashSet};

use super::codegraph::{CodeEdge, CodeGraph, EdgeKind, EdgeKindSet, NodeKey, Provenance};

/// Traversal direction over the directed graph.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Dir {
    /// Follow edges forward (from → to): what this node reaches.
    Out,
    /// Follow edges backward (to → from): what reaches this node.
    In,
    /// Both directions — the undirected neighborhood.
    Both,
}

impl Dir {
    /// Parse the tool `direction` param. Accepts the call-graph synonyms so an agent used to
    /// `call_graph` can reuse its vocabulary.
    pub(crate) fn parse(s: &str) -> Option<Dir> {
        match s {
            "out" | "callees" | "downstream" => Some(Dir::Out),
            "in" | "callers" | "upstream" => Some(Dir::In),
            "both" | "all" | "undirected" => Some(Dir::Both),
            _ => None,
        }
    }
}

/// Integer cost of crossing an edge, on the provenance ladder (ADR-0002): a proven edge is
/// cheaper than an inferred one, which is cheaper than an ambiguous one. Every cost is `>=
/// 10`, so hop count dominates and confidence breaks ties between equal-length routes.
pub(crate) fn edge_cost(provenance: Provenance) -> u32 {
    match provenance {
        Provenance::Extracted => 10,
        Provenance::Inferred => 15,
        Provenance::Ambiguous => 18,
    }
}

/// One incident edge in the interned adjacency. `other` is the node at the far end; the edge
/// always points `from → to` in the original graph regardless of which direction we walked.
#[derive(Debug, Clone, Copy)]
pub(crate) struct AdjEdge {
    pub(crate) other: u32,
    pub(crate) kind: EdgeKind,
    pub(crate) provenance: Provenance,
    pub(crate) weight: u32,
}

/// An interned, bidirectional adjacency view over a built [`CodeGraph`]. Node ids are dense
/// `u32` indices into `nodes`; `out[id]` and `inc[id]` hold the forward/backward incident
/// edges. Built once per query and discarded.
pub(crate) struct Adjacency {
    // Each node is stored once behind an `Arc` shared between `nodes` and `index_of`, so interning
    // a fresh node deep-clones the (heap `RelPath`) key only once and both sides hold a refcount.
    nodes: Vec<Arc<NodeKey>>,
    index_of: AHashMap<Arc<NodeKey>, u32>,
    out: Vec<Vec<AdjEdge>>,
    inc: Vec<Vec<AdjEdge>>,
}

impl Adjacency {
    /// Intern every node in `graph.edges` and build forward/backward adjacency lists.
    /// Iteration follows `graph.edges`, which `codegraph::build` already sorts, so the
    /// resulting order is deterministic.
    pub(crate) fn build(graph: &CodeGraph) -> Self {
        Self::build_from_edges(&graph.edges)
    }

    /// Build the adjacency directly from an edge slice. Lets a caller that applies a per-call
    /// confidence filter feed the memoized graph's edges by reference — borrowing the whole set
    /// when no filter is active — instead of cloning them into a throwaway [`CodeGraph`].
    pub(crate) fn build_from_edges(edges: &[CodeEdge]) -> Self {
        let mut adj = Adjacency {
            nodes: Vec::new(),
            index_of: AHashMap::new(),
            out: Vec::new(),
            inc: Vec::new(),
        };
        for e in edges {
            let from = adj.intern(&e.from);
            let to = adj.intern(&e.to);
            adj.out[from as usize].push(AdjEdge {
                other: to,
                kind: e.kind,
                provenance: e.provenance,
                weight: e.weight,
            });
            adj.inc[to as usize].push(AdjEdge {
                other: from,
                kind: e.kind,
                provenance: e.provenance,
                weight: e.weight,
            });
        }
        adj
    }

    /// Intern a node, allocating a fresh id (with empty adjacency lists) on first sight.
    /// Callers use it to add an isolated root that carries no edges.
    pub(crate) fn intern(&mut self, key: &NodeKey) -> u32 {
        if let Some(&id) = self.index_of.get(key) {
            return id;
        }
        let id = self.nodes.len() as u32;
        let k = Arc::new(key.clone());
        self.nodes.push(Arc::clone(&k));
        self.out.push(Vec::new());
        self.inc.push(Vec::new());
        self.index_of.insert(k, id);
        id
    }

    /// The id of an already-interned node, or `None` if the node is absent from the graph.
    pub(crate) fn id(&self, key: &NodeKey) -> Option<u32> {
        self.index_of.get(key).copied()
    }

    /// The node behind an id.
    pub(crate) fn node(&self, id: u32) -> &NodeKey {
        self.nodes[id as usize].as_ref()
    }

    /// Number of interned nodes.
    pub(crate) fn node_count(&self) -> usize {
        self.nodes.len()
    }

    /// Undirected incident edges of `id` for community detection: `(other_id, weight)` over both
    /// directions, unfiltered by kind/confidence. Weight folds provenance confidence in
    /// (`edge.weight * round(confidence * 10)`) so a proven edge pulls harder than an ambiguous
    /// one. Parallel edges are yielded separately; callers that need per-neighbor totals sum them.
    pub(crate) fn undirected_weighted(&self, id: u32) -> impl Iterator<Item = (u32, u64)> + '_ {
        let weigh = |e: &AdjEdge| e.weight as u64 * (e.provenance.confidence() * 10.0).round() as u64;
        self.out[id as usize]
            .iter()
            .map(move |e| (e.other, weigh(e)))
            .chain(self.inc[id as usize].iter().map(move |e| (e.other, weigh(e))))
    }

    /// Incident edges of `id` in traversal direction `dir`, filtered to the selected `kinds`
    /// and to edges whose confidence is at least `min_conf`. Each yielded tuple is the
    /// *directed* edge as it appears in the graph — `(from_id, to_id, AdjEdge)` — so callers
    /// record faithful direction even when walking `In`.
    fn incident<'a>(
        &'a self,
        id: u32,
        dir: Dir,
        kinds: EdgeKindSet,
        min_conf: f32,
    ) -> impl Iterator<Item = (u32, u32, AdjEdge)> + 'a {
        let forward = matches!(dir, Dir::Out | Dir::Both);
        let backward = matches!(dir, Dir::In | Dir::Both);
        let out = if forward { self.out[id as usize].as_slice() } else { &[] };
        let inc = if backward {
            self.inc[id as usize].as_slice()
        } else {
            &[]
        };
        out.iter()
            .map(move |e| (id, e.other, *e))
            .chain(inc.iter().map(move |e| (e.other, id, *e)))
            .filter(move |(_, _, e)| kinds.contains_kind(e.kind) && e.provenance.confidence() >= min_conf)
    }
}

/// An edge in a traversal result: endpoints as node ids plus the edge's typing.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct WalkEdge {
    pub(crate) from: u32,
    pub(crate) to: u32,
    pub(crate) kind: EdgeKind,
    pub(crate) provenance: Provenance,
    pub(crate) weight: u32,
}

/// A bounded neighborhood walk: the reached nodes (with hop depth from the nearest root) and
/// the edges among them.
#[derive(Debug, Default)]
pub(crate) struct Walk {
    /// `(node_id, depth)` in discovery order; roots are depth 0 and come first.
    pub(crate) nodes: Vec<(u32, u32)>,
    pub(crate) edges: Vec<WalkEdge>,
    /// True when `max_nodes` capped discovery before the neighborhood was exhausted.
    pub(crate) truncated: bool,
}

/// Bounds shared by the neighborhood walks.
#[derive(Debug, Clone, Copy)]
pub(crate) struct Bounds {
    pub(crate) depth: u32,
    pub(crate) max_nodes: usize,
    pub(crate) min_conf: f32,
}

/// N-hop expansion from `roots`, following `dir` over the selected `kinds`. Deterministic:
/// BFS in adjacency order (itself derived from the sorted graph), edges deduplicated by
/// `(from, to, kind)`. Stops adding *non-root* nodes once `max_nodes` is reached and flags
/// `truncated`. Roots are admitted unconditionally (they must be present), so a name resolving to
/// more than `max_nodes` definition sites can exceed the cap — bounded only by the repo's symbol count.
pub(crate) fn neighbors(adj: &Adjacency, roots: &[u32], dir: Dir, kinds: EdgeKindSet, bounds: Bounds) -> Walk {
    let mut walk = Walk::default();
    let mut depth_of: AHashMap<u32, u32> = AHashMap::new();
    let mut seen_edges: AHashSet<(u32, u32, EdgeKind)> = AHashSet::new();
    let mut queue: VecDeque<u32> = VecDeque::new();

    for &r in roots {
        if depth_of.contains_key(&r) {
            continue;
        }
        depth_of.insert(r, 0);
        walk.nodes.push((r, 0));
        queue.push_back(r);
    }

    // Phase 1 — discover the node set by BFS, bounded by depth and `max_nodes`. Only nodes are
    // admitted here; edges are collected in phase 2 once the set is final, so the result never
    // references a node the cap excluded (the `Walk` contract: edges only among returned nodes).
    while let Some(id) = queue.pop_front() {
        let depth = depth_of[&id];
        if depth >= bounds.depth {
            continue;
        }
        for (_, _, e) in adj.incident(id, dir, kinds, bounds.min_conf) {
            if depth_of.contains_key(&e.other) {
                continue;
            }
            if walk.nodes.len() >= bounds.max_nodes {
                walk.truncated = true;
                continue;
            }
            depth_of.insert(e.other, depth + 1);
            walk.nodes.push((e.other, depth + 1));
            queue.push_back(e.other);
        }
    }

    // Phase 2 — induced edges: every selected edge between two admitted nodes, including edges
    // among the outermost (max-depth) ring that phase 1's expansion never visits. Deduped by
    // `(from, to, kind)`; iteration follows the deterministic node-discovery order.
    for &(id, _) in &walk.nodes {
        for (from, to, e) in adj.incident(id, dir, kinds, bounds.min_conf) {
            if !depth_of.contains_key(&e.other) {
                continue;
            }
            if seen_edges.insert((from, to, e.kind)) {
                walk.edges.push(WalkEdge {
                    from,
                    to,
                    kind: e.kind,
                    provenance: e.provenance,
                    weight: e.weight,
                });
            }
        }
    }
    walk
}

/// A confidence-weighted shortest path: node ids from a source to a target, the edges that
/// connect them in order, and the total integer cost ([`edge_cost`] summed).
#[derive(Debug)]
pub(crate) struct Path {
    pub(crate) nodes: Vec<u32>,
    pub(crate) edges: Vec<WalkEdge>,
    pub(crate) cost: u32,
}

/// Confidence-weighted shortest directed path from any node in `sources` to any node in
/// `targets`, following forward edges over the selected `kinds`. Integer Dijkstra with a
/// deterministic `(cost, node_id)` tie-break; edges below `min_conf` are excluded. Returns
/// `None` when no target is reachable. `scan_cap` bounds the number of node relaxations so a
/// hub root cannot trigger unbounded work.
pub(crate) fn shortest_path(
    adj: &Adjacency,
    sources: &[u32],
    targets: &AHashSet<u32>,
    kinds: EdgeKindSet,
    min_conf: f32,
    scan_cap: usize,
) -> Option<Path> {
    let mut dist: AHashMap<u32, u32> = AHashMap::new();
    // prev[node] = (predecessor_id, edge into node)
    let mut prev: AHashMap<u32, (u32, WalkEdge)> = AHashMap::new();
    let mut heap: BinaryHeap<Reverse<(u32, u32)>> = BinaryHeap::new();

    for &s in sources {
        if dist.get(&s).is_none_or(|&d| d > 0) {
            dist.insert(s, 0);
            heap.push(Reverse((0, s)));
        }
    }

    let mut relaxations = 0usize;
    while let Some(Reverse((d, id))) = heap.pop() {
        if d > *dist.get(&id).unwrap_or(&u32::MAX) {
            continue; // stale heap entry
        }
        if targets.contains(&id) {
            return Some(reconstruct(id, &prev));
        }
        relaxations += 1;
        if relaxations > scan_cap {
            break;
        }
        // Forward edges only — `Out` — in adjacency order for a deterministic tie-break.
        for (from, to, e) in adj.incident(id, Dir::Out, kinds, min_conf) {
            let nd = d.saturating_add(edge_cost(e.provenance));
            if nd < *dist.get(&to).unwrap_or(&u32::MAX) {
                dist.insert(to, nd);
                prev.insert(
                    to,
                    (
                        from,
                        WalkEdge {
                            from,
                            to,
                            kind: e.kind,
                            provenance: e.provenance,
                            weight: e.weight,
                        },
                    ),
                );
                heap.push(Reverse((nd, to)));
            }
        }
    }
    None
}

/// Walk `prev` back from `target` to a source, producing the path in forward order.
fn reconstruct(target: u32, prev: &AHashMap<u32, (u32, WalkEdge)>) -> Path {
    let mut nodes = vec![target];
    let mut edges: Vec<WalkEdge> = Vec::new();
    let mut cost = 0u32;
    let mut cur = target;
    while let Some(&(pred, edge)) = prev.get(&cur) {
        edges.push(edge);
        cost = cost.saturating_add(edge_cost(edge.provenance));
        nodes.push(pred);
        cur = pred;
    }
    nodes.reverse();
    edges.reverse();
    Path { nodes, edges, cost }
}

/// A subgraph result: the kept nodes with their centrality score, and the induced edges.
#[derive(Debug, Default)]
pub(crate) struct Subgraph {
    /// `(node_id, score)` sorted by descending score; roots are always kept.
    pub(crate) nodes: Vec<(u32, u64)>,
    pub(crate) edges: Vec<WalkEdge>,
    pub(crate) truncated: bool,
}

/// The neighborhood around `roots` cut to the `max_keep` most central nodes. Gathers the
/// `Both`-direction neighborhood within `bounds.depth`, scores each node by local weighted
/// centrality (incident `weight * confidence`, integer), always keeps the roots, keeps the
/// top `max_keep` by score, and returns the edges induced among the kept set. Deterministic:
/// ties break by node id.
pub(crate) fn subgraph(
    adj: &Adjacency,
    roots: &[u32],
    kinds: EdgeKindSet,
    bounds: Bounds,
    max_keep: usize,
) -> Subgraph {
    let walk = neighbors(adj, roots, Dir::Both, kinds, bounds);
    if walk.nodes.is_empty() {
        return Subgraph::default();
    }

    // Local weighted centrality: sum of `weight * round(confidence*10)` over incident edges ~keep
    // within the gathered neighborhood. Integer, so ranking is exact. ~keep
    let mut score: AHashMap<u32, u64> = walk.nodes.iter().map(|&(id, _)| (id, 0u64)).collect();
    for e in &walk.edges {
        let w = e.weight as u64 * (e.provenance.confidence() * 10.0).round() as u64;
        if let Some(s) = score.get_mut(&e.from) {
            *s += w;
        }
        if let Some(s) = score.get_mut(&e.to) {
            *s += w;
        }
    }

    let root_set: AHashSet<u32> = roots.iter().copied().collect();
    let mut ranked: Vec<(u32, u64)> = score.into_iter().collect();
    // Roots first, then by descending score, then by id for determinism.
    ranked.sort_by(|a, b| {
        let ra = root_set.contains(&a.0);
        let rb = root_set.contains(&b.0);
        rb.cmp(&ra).then_with(|| b.1.cmp(&a.1)).then_with(|| a.0.cmp(&b.0))
    });

    // Never cut below the root set — the contract guarantees roots are always kept, even when
    // more names resolve to roots than `max_keep`.
    let keep = max_keep.max(root_set.len());
    let truncated = walk.truncated || ranked.len() > keep;
    ranked.truncate(keep);
    let kept: AHashSet<u32> = ranked.iter().map(|&(id, _)| id).collect();
    let edges: Vec<WalkEdge> = walk
        .edges
        .into_iter()
        .filter(|e| kept.contains(&e.from) && kept.contains(&e.to))
        .collect();

    Subgraph {
        nodes: ranked,
        edges,
        truncated,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn n(name: &str) -> NodeKey {
        NodeKey::Name(name.to_string())
    }

    fn edge(from: &str, to: &str, kind: EdgeKind, prov: Provenance, weight: u32) -> CodeEdge {
        CodeEdge {
            from: n(from),
            to: n(to),
            kind,
            provenance: prov,
            weight,
        }
    }

    /// A → B → C → D chain of calls, plus a low-confidence A → D shortcut, and an unrelated
    /// import edge E → B. Enough to exercise direction, depth, path weighting, and filtering.
    fn chain_graph() -> CodeGraph {
        CodeGraph {
            edges: vec![
                edge("A", "B", EdgeKind::Calls, Provenance::Extracted, 1),
                edge("B", "C", EdgeKind::Calls, Provenance::Extracted, 1),
                edge("C", "D", EdgeKind::Calls, Provenance::Extracted, 1),
                edge("A", "D", EdgeKind::Calls, Provenance::Ambiguous, 1),
                edge("E", "B", EdgeKind::Imports, Provenance::Inferred, 1),
            ],
            truncated: false,
        }
    }

    fn id_of(adj: &Adjacency, name: &str) -> u32 {
        adj.id(&n(name)).expect("node present")
    }

    fn names(adj: &Adjacency, ids: &[u32]) -> Vec<String> {
        ids.iter()
            .map(|&i| match adj.node(i) {
                NodeKey::Name(s) => s.clone(),
                other => format!("{other:?}"),
            })
            .collect()
    }

    const ALL: EdgeKindSet = EdgeKindSet {
        calls: true,
        imports: true,
        inherits: true,
        contains: true,
        annotates: false,
        cites: false,
        documents: false,
    };
    const CALLS: EdgeKindSet = EdgeKindSet {
        calls: true,
        imports: false,
        inherits: false,
        contains: false,
        annotates: false,
        cites: false,
        documents: false,
    };

    #[test]
    fn dir_parse_accepts_synonyms() {
        assert_eq!(Dir::parse("out"), Some(Dir::Out));
        assert_eq!(Dir::parse("callees"), Some(Dir::Out));
        assert_eq!(Dir::parse("in"), Some(Dir::In));
        assert_eq!(Dir::parse("callers"), Some(Dir::In));
        assert_eq!(Dir::parse("both"), Some(Dir::Both));
        assert_eq!(Dir::parse("bogus"), None);
    }

    #[test]
    fn edge_cost_follows_confidence_ladder() {
        assert!(edge_cost(Provenance::Extracted) < edge_cost(Provenance::Inferred));
        assert!(edge_cost(Provenance::Inferred) < edge_cost(Provenance::Ambiguous));
    }

    #[test]
    fn neighbors_out_respects_depth() {
        let g = chain_graph();
        let adj = Adjacency::build(&g);
        let a = id_of(&adj, "A");
        let walk = neighbors(
            &adj,
            &[a],
            Dir::Out,
            CALLS,
            Bounds {
                depth: 1,
                max_nodes: 100,
                min_conf: 0.0,
            },
        );
        let mut reached = names(&adj, &walk.nodes.iter().map(|&(id, _)| id).collect::<Vec<_>>());
        reached.sort();
        // depth 1 from A over calls: B (A→B) and D (A→D shortcut). Not C (2 hops).
        assert_eq!(reached, vec!["A", "B", "D"]);
    }

    #[test]
    fn neighbors_in_walks_backward() {
        let g = chain_graph();
        let adj = Adjacency::build(&g);
        let c = id_of(&adj, "C");
        let walk = neighbors(
            &adj,
            &[c],
            Dir::In,
            CALLS,
            Bounds {
                depth: 2,
                max_nodes: 100,
                min_conf: 0.0,
            },
        );
        let mut reached = names(&adj, &walk.nodes.iter().map(|&(id, _)| id).collect::<Vec<_>>());
        reached.sort();
        // who reaches C within 2 hops backward over calls: B (B→C), A (A→B→C).
        assert_eq!(reached, vec!["A", "B", "C"]);
    }

    #[test]
    fn neighbors_min_confidence_prunes_edges() {
        let g = chain_graph();
        let adj = Adjacency::build(&g);
        let a = id_of(&adj, "A");
        // floor above ambiguous (0.2) drops the A→D shortcut; only the extracted A→B remains.
        let walk = neighbors(
            &adj,
            &[a],
            Dir::Out,
            CALLS,
            Bounds {
                depth: 1,
                max_nodes: 100,
                min_conf: 0.5,
            },
        );
        let mut reached = names(&adj, &walk.nodes.iter().map(|&(id, _)| id).collect::<Vec<_>>());
        reached.sort();
        assert_eq!(reached, vec!["A", "B"]);
    }

    #[test]
    fn neighbors_kind_filter_selects_lane() {
        let g = chain_graph();
        let adj = Adjacency::build(&g);
        let b = id_of(&adj, "B");
        // Both directions, imports lane only: E→B is the sole imports edge touching B.
        let walk = neighbors(
            &adj,
            &[b],
            Dir::Both,
            EdgeKindSet {
                calls: false,
                imports: true,
                inherits: false,
                contains: false,
                annotates: false,
                cites: false,
                documents: false,
            },
            Bounds {
                depth: 1,
                max_nodes: 100,
                min_conf: 0.0,
            },
        );
        let mut reached = names(&adj, &walk.nodes.iter().map(|&(id, _)| id).collect::<Vec<_>>());
        reached.sort();
        assert_eq!(reached, vec!["B", "E"]);
    }

    #[test]
    fn neighbors_max_nodes_truncates() {
        let g = chain_graph();
        let adj = Adjacency::build(&g);
        let a = id_of(&adj, "A");
        let walk = neighbors(
            &adj,
            &[a],
            Dir::Both,
            ALL,
            Bounds {
                depth: 10,
                max_nodes: 2,
                min_conf: 0.0,
            },
        );
        assert!(walk.truncated, "capping node discovery flags truncated");
        assert_eq!(walk.nodes.len(), 2);
    }

    #[test]
    fn shortest_path_prefers_confidence_over_the_ambiguous_shortcut() {
        let g = chain_graph();
        let adj = Adjacency::build(&g);
        let a = id_of(&adj, "A");
        let d = id_of(&adj, "D");
        let targets: AHashSet<u32> = [d].into_iter().collect();
        let path = shortest_path(&adj, &[a], &targets, CALLS, 0.0, 10_000).expect("A reaches D");
        // Two routes A→D: the 1-hop ambiguous shortcut (cost 18) and A→B→C→D (3×10=30).
        // Fewer hops still wins here — the shortcut is the shortest.
        assert_eq!(names(&adj, &path.nodes), vec!["A", "D"]);
        assert_eq!(path.cost, edge_cost(Provenance::Ambiguous));
    }

    #[test]
    fn shortest_path_confidence_floor_forces_the_long_route() {
        let g = chain_graph();
        let adj = Adjacency::build(&g);
        let a = id_of(&adj, "A");
        let d = id_of(&adj, "D");
        let targets: AHashSet<u32> = [d].into_iter().collect();
        // Floor above ambiguous removes the shortcut, so the only route is the extracted chain.
        let path = shortest_path(&adj, &[a], &targets, CALLS, 0.5, 10_000).expect("chain still connects");
        assert_eq!(names(&adj, &path.nodes), vec!["A", "B", "C", "D"]);
        assert_eq!(path.cost, 3 * edge_cost(Provenance::Extracted));
    }

    #[test]
    fn shortest_path_returns_none_when_unreachable() {
        let g = chain_graph();
        let adj = Adjacency::build(&g);
        let d = id_of(&adj, "D");
        let a = id_of(&adj, "A");
        let targets: AHashSet<u32> = [a].into_iter().collect();
        // D has no outgoing edges — it cannot reach A.
        assert!(shortest_path(&adj, &[d], &targets, CALLS, 0.0, 10_000).is_none());
    }

    #[test]
    fn subgraph_keeps_roots_and_cuts_to_head() {
        let g = chain_graph();
        let adj = Adjacency::build(&g);
        let b = id_of(&adj, "B");
        let sg = subgraph(
            &adj,
            &[b],
            ALL,
            Bounds {
                depth: 3,
                max_nodes: 100,
                min_conf: 0.0,
            },
            2,
        );
        assert_eq!(sg.nodes.len(), 2, "cut to max_keep");
        assert_eq!(sg.nodes[0].0, b, "root is kept and ranked first");
        assert!(sg.truncated, "cutting below the neighborhood size flags truncated");
        // Every induced edge connects two kept nodes.
        let kept: AHashSet<u32> = sg.nodes.iter().map(|&(id, _)| id).collect();
        for e in &sg.edges {
            assert!(kept.contains(&e.from) && kept.contains(&e.to));
        }
    }

    /// A → B, A → C, B → C — a triangle where B and C both sit at depth 1 from A. Exercises
    /// edges among nodes on the outermost (max-depth) ring.
    fn triangle_graph() -> CodeGraph {
        CodeGraph {
            edges: vec![
                edge("A", "B", EdgeKind::Calls, Provenance::Extracted, 1),
                edge("A", "C", EdgeKind::Calls, Provenance::Extracted, 1),
                edge("B", "C", EdgeKind::Calls, Provenance::Extracted, 1),
            ],
            truncated: false,
        }
    }

    #[test]
    fn neighbors_max_nodes_emits_no_dangling_edge() {
        let g = chain_graph();
        let adj = Adjacency::build(&g);
        let a = id_of(&adj, "A");
        let walk = neighbors(
            &adj,
            &[a],
            Dir::Both,
            ALL,
            Bounds {
                depth: 10,
                max_nodes: 2,
                min_conf: 0.0,
            },
        );
        // Every edge endpoint must be a returned node — the cap must not leave a dangling edge.
        let node_set: AHashSet<u32> = walk.nodes.iter().map(|&(id, _)| id).collect();
        for e in &walk.edges {
            assert!(
                node_set.contains(&e.from) && node_set.contains(&e.to),
                "edge {:?}→{:?} references a node cut by max_nodes",
                e.from,
                e.to
            );
        }
    }

    #[test]
    fn neighbors_emits_edges_among_frontier_peers() {
        let g = triangle_graph();
        let adj = Adjacency::build(&g);
        let a = id_of(&adj, "A");
        let b = id_of(&adj, "B");
        let c = id_of(&adj, "C");
        // Depth 1 from A: B and C are both frontier nodes. The B→C edge between them must be
        // present even though neither frontier node is expanded.
        let walk = neighbors(
            &adj,
            &[a],
            Dir::Out,
            CALLS,
            Bounds {
                depth: 1,
                max_nodes: 100,
                min_conf: 0.0,
            },
        );
        assert!(
            walk.edges.iter().any(|e| e.from == b && e.to == c),
            "the B→C edge among two depth-1 frontier peers is missing"
        );
    }

    #[test]
    fn subgraph_keeps_all_roots_even_when_over_max_keep() {
        let g = chain_graph();
        let adj = Adjacency::build(&g);
        let roots = [id_of(&adj, "A"), id_of(&adj, "B"), id_of(&adj, "C")];
        // max_keep is below the root count; every root must still survive the cut.
        let sg = subgraph(
            &adj,
            &roots,
            ALL,
            Bounds {
                depth: 1,
                max_nodes: 100,
                min_conf: 0.0,
            },
            1,
        );
        let kept: AHashSet<u32> = sg.nodes.iter().map(|&(id, _)| id).collect();
        for r in roots {
            assert!(kept.contains(&r), "root {r:?} was dropped despite the keep guarantee");
        }
    }

    #[test]
    fn shortest_path_zero_when_source_is_target() {
        let g = chain_graph();
        let adj = Adjacency::build(&g);
        let a = id_of(&adj, "A");
        let targets: AHashSet<u32> = [a].into_iter().collect();
        let path = shortest_path(&adj, &[a], &targets, CALLS, 0.0, 10_000).expect("self is reachable");
        assert_eq!(names(&adj, &path.nodes), vec!["A"]);
        assert_eq!(path.cost, 0);
    }

    #[test]
    fn shortest_path_multi_target_picks_nearest() {
        let g = chain_graph();
        let adj = Adjacency::build(&g);
        let a = id_of(&adj, "A");
        let c = id_of(&adj, "C");
        let d = id_of(&adj, "D");
        // From A: D via the 1-hop ambiguous shortcut (cost 18) beats C via A→B→C (cost 20).
        let targets: AHashSet<u32> = [c, d].into_iter().collect();
        let path = shortest_path(&adj, &[a], &targets, CALLS, 0.0, 10_000).expect("a target is reachable");
        assert_eq!(names(&adj, &path.nodes), vec!["A", "D"]);
        assert_eq!(path.cost, edge_cost(Provenance::Ambiguous));
    }

    #[test]
    fn neighbors_is_deterministic() {
        let g = chain_graph();
        let adj = Adjacency::build(&g);
        let a = id_of(&adj, "A");
        let run = || {
            let w = neighbors(
                &adj,
                &[a],
                Dir::Both,
                ALL,
                Bounds {
                    depth: 10,
                    max_nodes: 100,
                    min_conf: 0.0,
                },
            );
            w.nodes.iter().map(|&(id, d)| (id, d)).collect::<Vec<_>>()
        };
        assert_eq!(run(), run());
    }
}