91 lines
3.7 KiB
Python
91 lines
3.7 KiB
Python
"""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",
|
|
})
|