"""Shared directory pruning policy for broad recursive scans. These names identify version-control internals, dependency trees, generated artifacts, caches, and backup copies that are not useful results for broad agent-facing discovery. Ordinary search callers may still target an explicit path; broad diagnostic probes should apply this policy to recursive walks. """ from __future__ import annotations from pathlib import Path SEARCH_RG_THREADS = 2 SEARCH_MAX_CONCURRENT = 2 _PROJECT_MARKERS = ('.git', '.hg', 'pyproject.toml', 'package.json', 'Cargo.toml', 'go.mod', 'CMakeLists.txt') def _project_root(path): """Prefer the enclosing repository over nested monorepo packages.""" package = None for parent in (path, *path.parents): if (parent / '.git').exists() or (parent / '.hg').exists(): return parent if package is None and any((parent / marker).exists() for marker in _PROJECT_MARKERS): package = parent return package def local_search_scope(path, cwd, home): """Resolve broad local roots without starting a recursive search process. Returns (root, notice, error). Only one level of candidate directories is inspected; no repository-wide discovery is needed to reject an umbrella. Real paths prevent symlink aliases from bypassing broad-root detection. """ root = Path(path) if not root.is_absolute(): root = Path(cwd) / root root = root.resolve() if not root.is_dir(): return path, None, None home = Path(home).resolve() project = _project_root(Path(cwd).resolve()) root_project = _project_root(root) if root_project is not None: return path, None, None candidates = [] try: # Bound metadata work even in directories containing many entries. from itertools import islice for child in islice(root.iterdir(), 256): if child.name in SEARCH_PRUNE_DIR_NAMES or not child.is_dir(): continue if any((child / marker).exists() for marker in _PROJECT_MARKERS): candidates.append(str(child)) except OSError: pass broad = (root == Path(root.anchor) or root == home or root in home.parents or len(candidates) > 1 or (project is not None and root in project.parents)) if not broad: return path, None, None if project is not None and root in project.parents: chosen = str(project) return chosen, f'Search scoped to current project: {chosen}. Set allow_broad=true for the requested umbrella.', None choices = ', '.join(sorted(candidates)[:12]) or 'choose an explicit project directory' return path, None, ('Search scope needed: requested root spans projects or a home/drive. ' f'Candidates: {choices}. Set allow_broad=true only for an intentional broad search.') # Keep this policy conservative and name-based so it works for local and remote # shell backends alike. The same set is used by context discovery and search # probes; adding a directory here protects every broad recursive consumer. SEARCH_PRUNE_DIR_NAMES = frozenset({ # Version-control internals. ".git", ".hg", ".svn", # Dependency and vendored trees. "node_modules", "venv", ".venv", "site-packages", "dist-packages", "vendor", "third_party", # Generated/build output. "build", "dist", "target", "out", "coverage", ".next", ".turbo", ".parcel-cache", ".nuxt", ".svelte-kit", # Python and package-manager caches. "__pycache__", ".cache", ".Trash", ".tox", ".nox", ".mypy_cache", ".pytest_cache", ".ruff_cache", ".npm", ".yarn", ".pnpm-store", ".gradle", ".m2", ".nuget", # Backup copies. "backups", "backup", ".backups", })