Searches for entry-point nodes matching the query and extracted symbols. Pipeline: 1. FTS search on the full query, each extracted symbol, stem variants, and agent-provided extra keywords. 2. Exact name supplement — ensures perfect name matches are never buried by BM25 noise. 3. Re-rank with structural signals (kind, visibility, path). 4. Connectivity boost (incoming call counts). 5. Co-occurrenc
(
&self,
query: &str,
symbols: &[String],
options: &BuildContextOptions,
)
| 176 | /// 6. Per-file diversity cap — limits how many symbols from a single file |
| 177 | /// appear so one large file doesn't dominate the output. |
| 178 | async fn find_entry_points( |
| 179 | &self, |
| 180 | query: &str, |
| 181 | symbols: &[String], |
| 182 | options: &BuildContextOptions, |
| 183 | ) -> Result<Vec<Node>> { |
| 184 | debug_assert!( |
| 185 | !query.is_empty(), |
| 186 | "find_entry_points called with empty query" |
| 187 | ); |
| 188 | debug_assert!(options.search_limit > 0, "search_limit must be positive"); |
| 189 | let mut seen_ids: HashSet<String> = options.exclude_node_ids.clone(); |
| 190 | let mut candidates: Vec<SearchResult> = Vec::new(); |
| 191 | let cap = options.max_nodes * 2; |
| 192 | |
| 193 | // Build a deduplicated, ordered list of FTS terms. Earlier revisions |
| 194 | // searched the full query, every extracted symbol, every stem, and |
| 195 | // every extra keyword separately — but these sets overlap heavily |
| 196 | // (e.g. `symbol` "foo" and `keyword` "foo" produce identical FTS |
| 197 | // results). libsql serializes queries on a single connection, so each |
| 198 | // duplicate term costs a full roundtrip. Order matters for the |
| 199 | // `cap`-based early exit, so we keep the original priority: |
| 200 | // full query → symbols → stems → extra keywords. |
| 201 | let mut fts_terms: Vec<String> = Vec::new(); |
| 202 | let mut fts_seen: HashSet<String> = HashSet::new(); |
| 203 | let push_term = |t: String, terms: &mut Vec<String>, seen: &mut HashSet<String>| { |
| 204 | if !t.is_empty() && seen.insert(t.clone()) { |
| 205 | terms.push(t); |
| 206 | } |
| 207 | }; |
| 208 | push_term(query.to_string(), &mut fts_terms, &mut fts_seen); |
| 209 | for s in symbols { |
| 210 | push_term(s.clone(), &mut fts_terms, &mut fts_seen); |
| 211 | } |
| 212 | let stems = generate_stem_variants(symbols); |
| 213 | for s in &stems { |
| 214 | push_term(s.clone(), &mut fts_terms, &mut fts_seen); |
| 215 | } |
| 216 | for k in &options.extra_keywords { |
| 217 | push_term(k.clone(), &mut fts_terms, &mut fts_seen); |
| 218 | } |
| 219 | |
| 220 | for term in &fts_terms { |
| 221 | if candidates.len() >= cap { |
| 222 | break; |
| 223 | } |
| 224 | let results = self.db.search_nodes(term, options.search_limit).await?; |
| 225 | for sr in results { |
| 226 | if Self::score_passes(sr.score, options.min_score) |
| 227 | && seen_ids.insert(sr.node.id.clone()) |
| 228 | { |
| 229 | candidates.push(sr); |
| 230 | } |
| 231 | } |
| 232 | } |
| 233 | |
| 234 | // --- Exact name supplement --- |
| 235 | // Ensures perfect name matches aren't buried by BM25 noise. |
no test coverage detected