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
70pub fn preview_repository_scope(
72 registration: &CodeRepositoryRegistration,
73 selector: &CodeRepositorySelector,
74) -> Result<CodeRepositoryScopePreview, CodeIndexError> {
75 let root = PathBuf::from(®istration.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
164pub 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(®istration.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(®istration.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, ®istration.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(®istration.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(®istration.path_filters, &selector.path_filters);
346 if !registration_scope_can_discover_source_roots(®istration.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, ®istration.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, ®istration.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 ®istration,
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 ®istration,
753 &selector
754 ));
755 assert!(default_source_preset_excludes("uv.lock"));
756 assert!(path_is_selected("uv.lock", ®istration, &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, ®istration, &selector), "{path}");
781 }
782 assert!(!path_is_selected(
783 "vendor/pkg/session_client.py",
784 ®istration,
785 &selector
786 ));
787 assert!(!path_is_selected(
788 "third_party/pkg/session_client.py",
789 ®istration,
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 ®istration,
810 &selector
811 ));
812 }
813}