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