Skip to main content

reflex/query/
result.rs

1//! Result assembly utilities: trigram index reconstruction and file-id resolution
2
3use anyhow::{Context, Result};
4
5use crate::content_store::ContentReader;
6use crate::trigram::TrigramIndex;
7
8/// Find a file_id by its path string in the content store.
9pub fn find_file_id(content_reader: &ContentReader, target_path: &str) -> Option<u32> {
10    for file_id in 0..content_reader.file_count() {
11        if let Some(path) = content_reader.get_file_path(file_id as u32)
12            && path.to_string_lossy() == target_path
13        {
14            return Some(file_id as u32);
15        }
16    }
17    None
18}
19
20/// Rebuild a trigram index from content store (fallback when trigrams.bin is missing).
21pub fn rebuild_trigram_index(content_reader: &ContentReader) -> Result<TrigramIndex> {
22    log::debug!(
23        "Rebuilding trigram index from {} files",
24        content_reader.file_count()
25    );
26    let mut trigram_index = TrigramIndex::new();
27
28    for file_id in 0..content_reader.file_count() {
29        let file_path = content_reader
30            .get_file_path(file_id as u32)
31            .context("Invalid file_id")?
32            .to_path_buf();
33        let content = content_reader.get_file_content(file_id as u32)?;
34
35        let idx = trigram_index.add_file(file_path);
36        trigram_index.index_file(idx, content);
37    }
38
39    trigram_index.finalize();
40    log::debug!(
41        "Trigram index rebuilt with {} trigrams",
42        trigram_index.trigram_count()
43    );
44
45    Ok(trigram_index)
46}
47
48/// Normalize a glob pattern so it matches the way indexed paths are stored.
49///
50/// Indexed paths are stored **relative and without a `./` prefix** (e.g.
51/// `src/parsers/rust.rs`) — see the `strip_prefix("./")` normalization applied
52/// throughout `query::mod`. A relative glob such as `src/**` must therefore be
53/// anchored so it can match those bare paths. We prepend `**/` (not `./`):
54/// `./src/**` fails to match `src/parsers/rust.rs` because of the leading `./`,
55/// whereas `**/src/**` matches it. This mirrors the convention the integration
56/// tests already rely on and is forgiving of LLM-authored patterns that omit a
57/// leading `**/` (REF-191).
58///
59/// Examples:
60/// - "src/**/*.rs" → "**/src/**/*.rs"
61/// - "main.rs"     → "**/main.rs"
62/// - "./services/**/*.php" → unchanged (already anchored)
63/// - "/abs/path"   → unchanged (absolute)
64/// - "**/foo"      → unchanged (already prefixed)
65pub fn normalize_glob_pattern(pattern: &str) -> String {
66    if pattern.starts_with('.') || pattern.starts_with('/') || pattern.starts_with('*') {
67        pattern.to_string()
68    } else {
69        format!("**/{}", pattern)
70    }
71}
72
73#[cfg(test)]
74mod normalize_glob_tests {
75    use super::normalize_glob_pattern;
76    use globset::Glob;
77
78    fn matches(pattern: &str, path: &str) -> bool {
79        let normalized = normalize_glob_pattern(pattern);
80        Glob::new(&normalized)
81            .unwrap()
82            .compile_matcher()
83            .is_match(path)
84    }
85
86    #[test]
87    fn relative_patterns_get_recursive_prefix() {
88        assert_eq!(normalize_glob_pattern("src/**/*.rs"), "**/src/**/*.rs");
89        assert_eq!(normalize_glob_pattern("main.rs"), "**/main.rs");
90        assert_eq!(normalize_glob_pattern("src/**"), "**/src/**");
91    }
92
93    #[test]
94    fn anchored_and_prefixed_patterns_are_unchanged() {
95        assert_eq!(
96            normalize_glob_pattern("./services/**/*.php"),
97            "./services/**/*.php"
98        );
99        assert_eq!(normalize_glob_pattern("/abs/path/*.rs"), "/abs/path/*.rs");
100        assert_eq!(normalize_glob_pattern("**/foo"), "**/foo");
101        assert_eq!(normalize_glob_pattern("*.rs"), "*.rs");
102    }
103
104    /// REF-191 regression: the natural `src/**` an LLM writes must match
105    /// bare stored paths like `src/parsers/rust.rs`. The old `./`-prefix
106    /// normalization returned zero matches for this, causing agents to
107    /// distrust Reflex and fall back to Grep on find-all tasks.
108    #[test]
109    fn src_glob_matches_bare_stored_paths() {
110        assert!(matches("src/**", "src/parsers/rust.rs"));
111        assert!(matches("src/**", "src/mcp.rs"));
112        assert!(matches("src/**/*.rs", "src/parsers/rust.rs"));
113        assert!(matches("src/**/*.rs", "src/mcp.rs"));
114    }
115
116    #[test]
117    fn src_glob_does_not_match_unrelated_paths() {
118        // Component boundary: `src` must be a whole path component.
119        assert!(!matches("src/**", "src_helpers/foo.rs"));
120        assert!(!matches("src/**/*.rs", "benches/foo.rs"));
121    }
122
123    #[test]
124    fn bare_filename_matches_at_any_depth() {
125        assert!(matches("main.rs", "src/main.rs"));
126        assert!(matches("main.rs", "main.rs"));
127    }
128}