Files
aiturk-hermes-ide/agent/search_policy.py
T
yilsem e7d3080e0a
Server CI and Windows package / verify (push) Failing after 1m40s
Server CI and Windows package / windows-package (push) Skipped
fix: preserve native model switches and bound project searches
2026-10-13 16:32:50 +03:00

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",
})