Skip to main content

relay_knowledge/code/
scope.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    fs,
4    path::{Path, PathBuf},
5};
6
7use crate::domain::{
8    CodeImpactPathGroups, CodeRepositoryExcludedPath, CodeRepositoryLanguagePreview,
9    CodeRepositoryLargestFile, CodeRepositoryRegistration, CodeRepositoryScopePreview,
10    CodeRepositorySelector,
11};
12
13use super::{
14    CodeIndexError,
15    changes::{GitTreeEntry, tracked_entries},
16    git_bytes, git_object_exists,
17    languages::language_id,
18    parser::dependency_manifest_language_ids,
19    parser::dependency_manifest_overrides_default_exclusion,
20    resolve_ref, resolve_tree,
21    source_roots::{NESTED_SOURCE_MARKERS, STRIPPABLE_SOURCE_ROOTS},
22};
23
24const PREVIEW_MAX_EXCLUDED_PATHS: usize = 50;
25const PREVIEW_MAX_LARGEST_FILES: usize = 10;
26const DEFAULT_TEXT_FILE_BUDGET_BYTES: usize = 512 * 1024;
27const DEFAULT_EXCLUDED_SEGMENTS: &[&str] = &[
28    ".git",
29    ".cache",
30    ".next",
31    ".nuxt",
32    ".parcel-cache",
33    ".pytest_cache",
34    ".ruff_cache",
35    ".tox",
36    ".venv",
37    "__pycache__",
38    "build",
39    "coverage",
40    "dist",
41    "node_modules",
42    "out",
43    "target",
44    "third_party",
45    "vendor",
46    "venv",
47];
48const DEFAULT_EXCLUDED_EXTENSIONS: &[&str] = &[
49    "7z", "avif", "bmp", "bz2", "class", "eot", "gif", "gz", "ico", "jar", "jpeg", "jpg", "jsonl",
50    "lockb", "map", "mov", "mp4", "otf", "pdf", "png", "svg", "tar", "tgz", "ttf", "wasm", "webm",
51    "woff", "woff2", "zip", "zst",
52];
53const DEFAULT_EXCLUDED_FILENAMES: &[&str] = &[".relay-knowledgeignore", "uv.lock"];
54const DEFAULT_DISTRIBUTION_SEGMENT: &str = "dist";
55const DEFAULT_DISTRIBUTION_LANGUAGE_SEGMENTS: &[&str] = &[
56    "javascript",
57    "js",
58    "src",
59    "source",
60    "sources",
61    "ts",
62    "typescript",
63];
64const DEFAULT_DISTRIBUTION_RUNTIME_SEGMENTS: &[&str] =
65    &["app", "client", "core", "runtime", "server"];
66const SOURCE_LAYOUT_DISCOVERY_MAX_PATHS: usize = 200_000;
67const SOURCE_LAYOUT_DISCOVERY_MAX_ROOTS: usize = 512;
68const AUTO_SOURCE_SCOPE_FILTERS: &[&str] = &[".", "src", "include", "lib", "Sources"];
69
70/// Returns a non-mutating preview of the effective repository indexing scope.
71pub fn preview_repository_scope(
72    registration: &CodeRepositoryRegistration,
73    selector: &CodeRepositorySelector,
74) -> Result<CodeRepositoryScopePreview, CodeIndexError> {
75    let root = PathBuf::from(&registration.root_path);
76    let commit = resolve_ref(&root, &selector.ref_selector)?;
77    let tree_hash = resolve_tree(&root, &commit)?;
78    let ignore_rules = load_ignore_rules_from_commit(&root, &commit)?;
79    let mut selected_byte_count = 0usize;
80    let mut selected_file_count = 0usize;
81    let mut unsupported_file_count = 0usize;
82    let mut generated_or_heavy_file_count = 0usize;
83    let mut expected_degraded_file_count = 0usize;
84    let mut language_distribution = BTreeMap::<String, (usize, usize)>::new();
85    let mut largest_files = Vec::<CodeRepositoryLargestFile>::new();
86    let mut excluded_paths = Vec::<CodeRepositoryExcludedPath>::new();
87
88    let entries = tracked_entries(&root, &commit)?;
89    let source_layout = discover_source_layout(&entries);
90    for entry in entries {
91        if let Some(reason) = selection_exclusion_reason_with_layout(
92            &entry.path,
93            registration,
94            selector,
95            &ignore_rules,
96            &source_layout,
97        ) {
98            if excluded_paths.len() < PREVIEW_MAX_EXCLUDED_PATHS {
99                excluded_paths.push(CodeRepositoryExcludedPath {
100                    path: entry.path,
101                    reason,
102                });
103            }
104            continue;
105        }
106        let language = preview_language_id(&entry.path);
107        selected_file_count += 1;
108        selected_byte_count = selected_byte_count.saturating_add(entry.byte_count);
109        let bucket = language_distribution
110            .entry(language.to_owned())
111            .or_insert((0, 0));
112        bucket.0 += 1;
113        bucket.1 = bucket.1.saturating_add(entry.byte_count);
114        let is_unsupported = language == "unknown";
115        let is_heavy = entry.byte_count > DEFAULT_TEXT_FILE_BUDGET_BYTES;
116        if is_unsupported {
117            unsupported_file_count += 1;
118        }
119        if is_heavy {
120            generated_or_heavy_file_count += 1;
121        }
122        if is_unsupported || is_heavy {
123            expected_degraded_file_count += 1;
124        }
125        largest_files.push(CodeRepositoryLargestFile {
126            path: entry.path,
127            byte_count: entry.byte_count,
128        });
129    }
130    largest_files.sort_by(|left, right| {
131        right
132            .byte_count
133            .cmp(&left.byte_count)
134            .then_with(|| left.path.cmp(&right.path))
135    });
136    largest_files.truncate(PREVIEW_MAX_LARGEST_FILES);
137
138    Ok(CodeRepositoryScopePreview {
139        repository_id: registration.repository_id.clone(),
140        alias: registration.alias.clone(),
141        requested_ref: selector.ref_selector.clone(),
142        resolved_commit_sha: commit,
143        tree_hash,
144        selected_file_count,
145        selected_byte_count,
146        unsupported_file_count,
147        generated_or_heavy_file_count,
148        expected_degraded_file_count,
149        language_distribution: language_distribution
150            .into_iter()
151            .map(
152                |(language_id, (file_count, byte_count))| CodeRepositoryLanguagePreview {
153                    language_id,
154                    file_count,
155                    byte_count,
156                },
157            )
158            .collect(),
159        largest_files,
160        excluded_paths,
161    })
162}
163
164/// Splits diff paths by the same selector rules used by indexing and impact.
165pub fn partition_changed_paths_for_selector(
166    registration: &CodeRepositoryRegistration,
167    selector: &CodeRepositorySelector,
168    paths: Vec<String>,
169) -> Result<CodeImpactPathGroups, CodeIndexError> {
170    let root = PathBuf::from(&registration.root_path);
171    let commit = resolve_ref(&root, &selector.ref_selector)?;
172    let ignore_rules = load_ignore_rules_from_commit(&root, &commit)?;
173    let entries = tracked_entries(&root, &commit)?;
174    let source_layout = discover_source_layout(&entries);
175    let mut in_scope_changed_paths = Vec::new();
176    let mut out_of_scope_changed_paths = Vec::new();
177    for path in paths {
178        if selection_exclusion_reason_with_layout(
179            &path,
180            registration,
181            selector,
182            &ignore_rules,
183            &source_layout,
184        )
185        .is_none()
186        {
187            in_scope_changed_paths.push(path);
188        } else {
189            out_of_scope_changed_paths.push(path);
190        }
191    }
192    in_scope_changed_paths.sort();
193    in_scope_changed_paths.dedup();
194    out_of_scope_changed_paths.sort();
195    out_of_scope_changed_paths.dedup();
196
197    Ok(CodeImpactPathGroups {
198        in_scope_changed_paths,
199        out_of_scope_changed_paths,
200    })
201}
202
203#[cfg(test)]
204pub(super) fn path_is_selected(
205    path: &str,
206    registration: &CodeRepositoryRegistration,
207    selector: &CodeRepositorySelector,
208) -> bool {
209    let root = Path::new(&registration.root_path);
210    let ignore_rules = load_ignore_rules(root).expect("ignore rules should load in tests");
211
212    path_is_selected_with_rules(path, registration, selector, &ignore_rules)
213}
214
215pub(super) fn path_is_selected_with_rules(
216    path: &str,
217    registration: &CodeRepositoryRegistration,
218    selector: &CodeRepositorySelector,
219    ignore_rules: &[IgnoreRule],
220) -> bool {
221    selection_exclusion_reason(path, registration, selector, ignore_rules).is_none()
222}
223
224pub(super) fn path_is_selected_with_layout(
225    path: &str,
226    registration: &CodeRepositoryRegistration,
227    selector: &CodeRepositorySelector,
228    ignore_rules: &[IgnoreRule],
229    source_layout: &SourceLayoutDiscovery,
230) -> bool {
231    selection_exclusion_reason_with_layout(
232        path,
233        registration,
234        selector,
235        ignore_rules,
236        source_layout,
237    )
238    .is_none()
239}
240
241pub(super) fn selection_exclusion_reason(
242    path: &str,
243    registration: &CodeRepositoryRegistration,
244    selector: &CodeRepositorySelector,
245    ignore_rules: &[IgnoreRule],
246) -> Option<String> {
247    selection_exclusion_reason_with_layout(
248        path,
249        registration,
250        selector,
251        ignore_rules,
252        &SourceLayoutDiscovery::default(),
253    )
254}
255
256pub(super) fn selection_exclusion_reason_with_layout(
257    path: &str,
258    registration: &CodeRepositoryRegistration,
259    selector: &CodeRepositorySelector,
260    ignore_rules: &[IgnoreRule],
261    source_layout: &SourceLayoutDiscovery,
262) -> Option<String> {
263    if !path_scope_allows(path, registration, selector)
264        && !source_layout.extends_path_scope(path, registration, selector)
265    {
266        return Some("outside registered/requested path scope".to_owned());
267    }
268    if !language_filter_allows(path, &registration.language_filters)
269        || !language_filter_allows(path, &selector.language_filters)
270    {
271        return Some("outside registered/requested language scope".to_owned());
272    }
273    if ignore_rules.iter().any(|rule| rule.matches(path)) {
274        return Some("excluded by .relay-knowledgeignore".to_owned());
275    }
276    if default_source_preset_excludes(path)
277        && !dependency_manifest_overrides_default_exclusion(path)
278        && !source_layout.keeps_default_excluded_source(path)
279        && !explicit_path_filter_opts_into_default_exclusion(
280            path,
281            registration
282                .path_filters
283                .iter()
284                .chain(selector.path_filters.iter()),
285        )
286    {
287        return Some("excluded by source preset".to_owned());
288    }
289
290    None
291}
292
293#[derive(Debug, Clone, Default, PartialEq, Eq)]
294pub(super) struct SourceLayoutDiscovery {
295    source_roots: BTreeSet<String>,
296}
297
298impl SourceLayoutDiscovery {
299    fn keeps_default_excluded_source(&self, path: &str) -> bool {
300        source_path_has_indexable_content(path)
301            && !path_contains_broad_dependency_segment(path)
302            && self
303                .source_roots
304                .iter()
305                .any(|root| path_matches_filter(path, root))
306    }
307
308    fn extends_path_scope(
309        &self,
310        path: &str,
311        registration: &CodeRepositoryRegistration,
312        selector: &CodeRepositorySelector,
313    ) -> bool {
314        registration_scope_can_discover_source_roots(&registration.path_filters)
315            && selector_path_scope_allows_discovered_root(path, &selector.path_filters)
316            && self.keeps_default_excluded_source(path)
317    }
318}
319
320pub(super) fn discover_source_layout(entries: &[GitTreeEntry]) -> SourceLayoutDiscovery {
321    let mut source_roots = BTreeSet::new();
322    for entry in entries.iter().take(SOURCE_LAYOUT_DISCOVERY_MAX_PATHS) {
323        if !source_path_has_indexable_content(&entry.path)
324            || path_contains_broad_dependency_segment(&entry.path)
325            || default_source_preset_excludes(&entry.path)
326        {
327            continue;
328        }
329        for root in source_layout_roots_for_path(&entry.path) {
330            source_roots.insert(root);
331            if source_roots.len() >= SOURCE_LAYOUT_DISCOVERY_MAX_ROOTS {
332                return SourceLayoutDiscovery { source_roots };
333            }
334        }
335    }
336
337    SourceLayoutDiscovery { source_roots }
338}
339
340pub(super) fn effective_index_path_filters(
341    registration: &CodeRepositoryRegistration,
342    selector: &CodeRepositorySelector,
343    source_layout: &SourceLayoutDiscovery,
344) -> Vec<String> {
345    let mut filters = merged_path_filters(&registration.path_filters, &selector.path_filters);
346    if !registration_scope_can_discover_source_roots(&registration.path_filters) {
347        return filters;
348    }
349    for root in &source_layout.source_roots {
350        if !selector_filter_allows_root(root, &selector.path_filters) {
351            continue;
352        }
353        push_filter_if_uncovered(&mut filters, root);
354    }
355
356    filters
357}
358
359fn source_path_has_indexable_content(path: &str) -> bool {
360    language_id(path).is_some() || dependency_manifest_language_ids(path).is_some()
361}
362
363fn preview_language_id(path: &str) -> &'static str {
364    language_id(path).unwrap_or_else(|| {
365        dependency_manifest_language_ids(path)
366            .and_then(|languages| languages.first().copied())
367            .unwrap_or("unknown")
368    })
369}
370
371fn path_contains_broad_dependency_segment(path: &str) -> bool {
372    normalize_path_filter(path)
373        .split('/')
374        .any(|segment| matches!(segment, "vendor" | "third_party" | "node_modules"))
375}
376
377fn registration_scope_can_discover_source_roots(filters: &[String]) -> bool {
378    !filters.is_empty()
379        && filters.iter().all(|filter| {
380            let filter = normalize_path_filter(filter);
381            AUTO_SOURCE_SCOPE_FILTERS.contains(&filter)
382        })
383}
384
385fn selector_path_scope_allows_discovered_root(path: &str, filters: &[String]) -> bool {
386    filters.is_empty()
387        || filters
388            .iter()
389            .any(|filter| path_matches_filter(path, filter))
390}
391
392fn selector_filter_allows_root(root: &str, filters: &[String]) -> bool {
393    filters.is_empty()
394        || filters
395            .iter()
396            .any(|filter| path_matches_filter(root, filter) || path_overlaps_filter(root, filter))
397}
398
399fn merged_path_filters(left: &[String], right: &[String]) -> Vec<String> {
400    let mut merged = Vec::new();
401    for filter in left.iter().chain(right.iter()) {
402        let normalized = normalize_path_filter(filter);
403        if !normalized.is_empty() && !merged.iter().any(|existing| existing == normalized) {
404            merged.push(normalized.to_owned());
405        }
406    }
407
408    merged
409}
410
411fn push_filter_if_uncovered(filters: &mut Vec<String>, root: &str) {
412    if filters
413        .iter()
414        .any(|filter| path_filter_covers(filter, root))
415    {
416        return;
417    }
418    filters.retain(|filter| !path_filter_covers(root, filter));
419    filters.push(root.to_owned());
420}
421
422fn path_filter_covers(filter: &str, path: &str) -> bool {
423    let filter = normalize_path_filter(filter);
424    filter == "." || path_matches_filter(path, filter)
425}
426
427fn source_layout_roots_for_path(path: &str) -> Vec<String> {
428    let path = normalize_path_filter(path);
429    let mut roots = Vec::new();
430    for marker in NESTED_SOURCE_MARKERS {
431        if let Some((prefix, _)) = path.split_once(marker) {
432            push_source_root(&mut roots, format!("{prefix}{marker}"));
433        }
434    }
435    for root in STRIPPABLE_SOURCE_ROOTS {
436        if let Some(suffix) = path.strip_prefix(root) {
437            let mut segments = suffix.split('/').filter(|segment| !segment.is_empty());
438            if let Some(first) = segments.next() {
439                push_source_root(&mut roots, format!("{root}{first}"));
440            } else {
441                push_source_root(&mut roots, root.trim_end_matches('/').to_owned());
442            }
443        }
444    }
445    roots
446}
447
448fn push_source_root(roots: &mut Vec<String>, root: String) {
449    let root = root.trim_end_matches('/').to_owned();
450    if !root.is_empty() && !roots.contains(&root) {
451        roots.push(root);
452    }
453}
454
455pub(super) fn path_scope_allows(
456    path: &str,
457    registration: &CodeRepositoryRegistration,
458    selector: &CodeRepositorySelector,
459) -> bool {
460    path_filter_allows(path, &registration.path_filters)
461        && path_filter_allows(path, &selector.path_filters)
462}
463
464pub(super) fn path_scope_overlaps(
465    path: &str,
466    registration: &CodeRepositoryRegistration,
467    selector: &CodeRepositorySelector,
468) -> bool {
469    path_filter_overlaps(path, &registration.path_filters)
470        && path_filter_overlaps(path, &selector.path_filters)
471}
472
473pub(super) fn load_ignore_rules(root: &Path) -> Result<Vec<IgnoreRule>, CodeIndexError> {
474    let path = root.join(".relay-knowledgeignore");
475    let content = match fs::read_to_string(path) {
476        Ok(content) => content,
477        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
478        Err(error) => return Err(error.into()),
479    };
480
481    Ok(parse_ignore_rules(&content))
482}
483
484pub(super) fn load_ignore_rules_from_commit(
485    root: &Path,
486    commit: &str,
487) -> Result<Vec<IgnoreRule>, CodeIndexError> {
488    let object = format!("{commit}:.relay-knowledgeignore");
489    if !git_object_exists(root, &object)? {
490        return Ok(Vec::new());
491    }
492    let content = String::from_utf8(git_bytes(root, ["show", &object])?).map_err(|error| {
493        CodeIndexError::InvalidInput(format!(
494            ".relay-knowledgeignore at {commit} is not valid UTF-8: {}",
495            error.utf8_error()
496        ))
497    })?;
498
499    Ok(parse_ignore_rules(&content))
500}
501
502fn parse_ignore_rules(content: &str) -> Vec<IgnoreRule> {
503    content
504        .lines()
505        .map(str::trim)
506        .filter(|line| !line.is_empty() && !line.starts_with('#') && !line.starts_with('!'))
507        .map(|line| IgnoreRule {
508            pattern: line.trim_start_matches('/').to_owned(),
509            anchored: line.starts_with('/'),
510        })
511        .collect()
512}
513
514#[derive(Debug, Clone, PartialEq, Eq)]
515pub(super) struct IgnoreRule {
516    pattern: String,
517    anchored: bool,
518}
519
520impl IgnoreRule {
521    fn matches(&self, path: &str) -> bool {
522        let pattern = normalize_path_filter(&self.pattern);
523        let path = normalize_path_filter(path);
524        if pattern.is_empty() {
525            return false;
526        }
527        if let Some(extension) = pattern.strip_prefix("*.") {
528            return if self.anchored {
529                path.rsplit_once('/').is_none()
530                    && path
531                        .rsplit_once('.')
532                        .is_some_and(|(_, path_extension)| path_extension == extension)
533            } else {
534                path.rsplit_once('.')
535                    .is_some_and(|(_, path_extension)| path_extension == extension)
536            };
537        }
538        if pattern.contains('/') {
539            return path == pattern || path.starts_with(&format!("{pattern}/"));
540        }
541        if self.anchored {
542            return path == pattern || path.starts_with(&format!("{pattern}/"));
543        }
544        path.split('/').any(|segment| segment == pattern)
545    }
546}
547
548fn path_filter_allows(path: &str, filters: &[String]) -> bool {
549    filters.is_empty()
550        || filters
551            .iter()
552            .any(|filter| path_matches_filter(path, filter))
553}
554
555fn path_filter_overlaps(path: &str, filters: &[String]) -> bool {
556    filters.is_empty()
557        || filters
558            .iter()
559            .any(|filter| path_overlaps_filter(path, filter))
560}
561
562fn language_filter_allows(path: &str, filters: &[String]) -> bool {
563    if filters.is_empty() {
564        return true;
565    }
566    if language_id(path).is_some_and(|language| filters.iter().any(|filter| filter == language)) {
567        return true;
568    }
569    dependency_manifest_language_ids(path).is_some_and(|languages| {
570        languages
571            .iter()
572            .any(|language| filters.iter().any(|filter| filter == language))
573    })
574}
575
576fn default_source_preset_excludes(path: &str) -> bool {
577    let normalized = normalize_path_filter(path);
578    if normalized
579        .rsplit('/')
580        .next()
581        .is_some_and(|file_name| DEFAULT_EXCLUDED_FILENAMES.contains(&file_name))
582    {
583        return true;
584    }
585    if normalized
586        .split('/')
587        .any(|segment| default_excluded_segment_excludes_path(segment, normalized))
588    {
589        return true;
590    }
591    normalized
592        .rsplit_once('.')
593        .map(|(_, extension)| {
594            DEFAULT_EXCLUDED_EXTENSIONS.contains(&extension.to_ascii_lowercase().as_str())
595        })
596        .unwrap_or(false)
597}
598
599fn default_excluded_segment_excludes_path(segment: &str, path: &str) -> bool {
600    DEFAULT_EXCLUDED_SEGMENTS.contains(&segment)
601        && (segment != DEFAULT_DISTRIBUTION_SEGMENT || distribution_segment_excludes_path(path))
602}
603
604fn distribution_segment_excludes_path(path: &str) -> bool {
605    let segments = path.split('/').collect::<Vec<_>>();
606
607    !distribution_runtime_source_path_is_indexable(path, &segments)
608}
609
610fn distribution_runtime_source_path_is_indexable(path: &str, segments: &[&str]) -> bool {
611    language_id(path).is_some()
612        && !path
613            .rsplit('/')
614            .next()
615            .is_some_and(|file_name| file_name.to_ascii_lowercase().contains(".min."))
616        && segments.windows(3).any(|window| {
617            window[0] == DEFAULT_DISTRIBUTION_SEGMENT
618                && DEFAULT_DISTRIBUTION_LANGUAGE_SEGMENTS.contains(&window[1])
619                && DEFAULT_DISTRIBUTION_RUNTIME_SEGMENTS.contains(&window[2])
620        })
621}
622
623fn explicit_path_filter_opts_into_default_exclusion<'a>(
624    path: &str,
625    filters: impl IntoIterator<Item = &'a String>,
626) -> bool {
627    let path_extension = path
628        .rsplit_once('.')
629        .map(|(_, extension)| extension.to_ascii_lowercase());
630    filters.into_iter().any(|filter| {
631        let filter = normalize_path_filter(filter);
632        if filter.is_empty() || filter == "." {
633            return false;
634        }
635        let filter_segments = filter.split('/').collect::<Vec<_>>();
636        let targets_default_exclusion = filter_segments.iter().any(|segment| {
637            DEFAULT_EXCLUDED_SEGMENTS.contains(segment)
638                || DEFAULT_EXCLUDED_FILENAMES.contains(segment)
639                || segment
640                    .rsplit_once('.')
641                    .map(|(_, ext)| ext.to_ascii_lowercase())
642                    .is_some_and(|extension| {
643                        DEFAULT_EXCLUDED_EXTENSIONS.contains(&extension.as_str())
644                    })
645        });
646        if !targets_default_exclusion {
647            return false;
648        }
649        path_matches_filter(path, filter)
650            || filter.strip_prefix("*.").is_some_and(|extension| {
651                path_extension.as_deref() == Some(&extension.to_ascii_lowercase())
652            })
653    })
654}
655
656fn path_matches_filter(path: &str, filter: &str) -> bool {
657    let path = normalize_path_filter(path);
658    let filter = normalize_path_filter(filter);
659    if filter == "." {
660        return true;
661    }
662    !filter.is_empty() && (path == filter || path.starts_with(&format!("{filter}/")))
663}
664
665fn path_overlaps_filter(path: &str, filter: &str) -> bool {
666    let path = normalize_path_filter(path);
667    let filter = normalize_path_filter(filter);
668    if filter == "." {
669        return true;
670    }
671    !path.is_empty()
672        && !filter.is_empty()
673        && (path == filter
674            || path.starts_with(&format!("{filter}/"))
675            || filter.starts_with(&format!("{path}/")))
676}
677
678fn normalize_path_filter(filter: &str) -> &str {
679    let mut filter = filter.trim_end_matches(['/', '\\']);
680    while let Some(stripped) = filter.strip_prefix("./") {
681        filter = stripped;
682    }
683
684    filter
685}
686
687#[cfg(test)]
688mod tests {
689    use crate::domain::{CodeRepositoryRegistration, CodeRepositorySelector};
690
691    use super::*;
692
693    #[test]
694    fn source_preset_keeps_distribution_runtime_sources_indexable() {
695        assert!(!default_source_preset_excludes(
696            "frontend/dist/js/core/stream.js"
697        ));
698        assert!(!default_source_preset_excludes(
699            "frontend/dist/js/app/bootstrap.js"
700        ));
701        assert!(!default_source_preset_excludes(
702            "web/dist/src/runtime/session.ts"
703        ));
704        assert!(default_source_preset_excludes("dist/bundle.js"));
705        assert!(default_source_preset_excludes(
706            "frontend/dist/js/components/sidebar.js"
707        ));
708        assert!(default_source_preset_excludes(
709            "frontend/dist/js/core/highlight.min.js"
710        ));
711        assert!(default_source_preset_excludes("frontend/dist/css/app.css"));
712        assert!(default_source_preset_excludes(
713            "node_modules/pkg/dist/js/core/index.js"
714        ));
715    }
716
717    #[test]
718    fn explicit_default_exclusion_opt_in_normalizes_extension_case() {
719        let registration = CodeRepositoryRegistration::new(
720            "repo",
721            "alias",
722            "/tmp/repo",
723            vec!["assets/logo.SVG".to_owned()],
724            Vec::new(),
725        )
726        .expect("registration should validate");
727        let selector = CodeRepositorySelector::new("alias", "HEAD", Vec::new(), Vec::new())
728            .expect("selector should validate");
729
730        assert!(path_is_selected(
731            "assets/logo.SVG",
732            &registration,
733            &selector
734        ));
735    }
736
737    #[test]
738    fn default_source_preset_excludes_dataset_dumps_and_uv_lock() {
739        let registration = CodeRepositoryRegistration::new(
740            "repo",
741            "alias",
742            "/tmp/repo",
743            vec![".".to_owned()],
744            Vec::new(),
745        )
746        .expect("registration should validate");
747        let selector = CodeRepositorySelector::new("alias", "HEAD", Vec::new(), Vec::new())
748            .expect("selector should validate");
749
750        assert!(!path_is_selected(
751            ".agent_teams/evals/datasets/swebench-verified-full.jsonl",
752            &registration,
753            &selector
754        ));
755        assert!(default_source_preset_excludes("uv.lock"));
756        assert!(path_is_selected("uv.lock", &registration, &selector));
757    }
758
759    #[test]
760    fn nonstandard_source_roots_are_selected_without_opt_in() {
761        let registration = CodeRepositoryRegistration::new(
762            "repo",
763            "alias",
764            "/tmp/repo",
765            vec![".".to_owned()],
766            Vec::new(),
767        )
768        .expect("registration should validate");
769        let selector = CodeRepositorySelector::new("alias", "HEAD", Vec::new(), Vec::new())
770            .expect("selector should validate");
771
772        for path in [
773            "external_deps/python_sdk/session_client.py",
774            "packages/ui/src/index.ts",
775            "modules/java_sdk/src/main/java/example/SessionClient.java",
776            "plugins/example.com/nonstandard/session/client.go",
777            "Sources/SwiftSdk/SessionClient.swift",
778            "lib/app/controller.rb",
779        ] {
780            assert!(path_is_selected(path, &registration, &selector), "{path}");
781        }
782        assert!(!path_is_selected(
783            "vendor/pkg/session_client.py",
784            &registration,
785            &selector
786        ));
787        assert!(!path_is_selected(
788            "third_party/pkg/session_client.py",
789            &registration,
790            &selector
791        ));
792    }
793
794    #[test]
795    fn explicit_vendor_source_opt_in_stays_supported() {
796        let registration = CodeRepositoryRegistration::new(
797            "repo",
798            "alias",
799            "/tmp/repo",
800            vec![".".to_owned(), "vendor".to_owned()],
801            Vec::new(),
802        )
803        .expect("registration should validate");
804        let selector = CodeRepositorySelector::new("alias", "HEAD", Vec::new(), Vec::new())
805            .expect("selector should validate");
806
807        assert!(path_is_selected(
808            "vendor/pkg/session_client.py",
809            &registration,
810            &selector
811        ));
812    }
813}