From bc8ca28c82dc2e1924d6a3135c9454961ca8f981 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Tue, 1 Sep 2026 23:14:29 +0200 Subject: [PATCH 1/2] minor changes to auditor --- .../agents/reviewer/modules/auditor.py | 17 ++--------- src/codespy/agents/reviewer/reviewer.py | 29 ------------------- 2 files changed, 3 insertions(+), 43 deletions(-) diff --git a/src/codespy/agents/reviewer/modules/auditor.py b/src/codespy/agents/reviewer/modules/auditor.py index 7121293..90ef985 100644 --- a/src/codespy/agents/reviewer/modules/auditor.py +++ b/src/codespy/agents/reviewer/modules/auditor.py @@ -1,7 +1,6 @@ """Auditor module — assesses code quality and provides recommendation after reviews.""" import logging -from collections.abc import Sequence from typing import TYPE_CHECKING import dspy @@ -13,7 +12,6 @@ from codespy.agents.reviewer.modules.scope_resolver import _deepest_common_folder from codespy.config import get_settings from codespy.config_memory import get_memory_store -from codespy.tools.git.models import ChangedFile if TYPE_CHECKING: from codespy.agents.reviewer.models import ScopeResult @@ -34,9 +32,6 @@ class AuditSignature(dspy.Signature): pr_title: str = dspy.InputField(desc="Title of the pull request") summary: str = dspy.InputField(desc="Summary of what this PR accomplishes") - changed_files: list[ChangedFile] = dspy.InputField( - desc="In-scope reviewable files with status and line counts" - ) all_issues: list[Issue] = dspy.InputField(desc="All issues found during review") quality_assessment: str = dspy.OutputField(desc="Overall assessment of code quality") @@ -55,9 +50,8 @@ def __init__(self) -> None: def _call_auditor( self, - auditor: dspy.ChainOfThought, + auditor: dspy.Module, review_context: ReviewContext, - audit_files: list[ChangedFile], all_issues: list[Issue], run_id: str | None, scopes: list["ScopeResult"] | None, @@ -98,7 +92,6 @@ def _call_auditor( result = mem( pr_title=review_context.pr_context.pr_title, summary=review_context.pr_context.summary, - changed_files=audit_files, all_issues=all_issues, ) # Run episode save synchronously (auditor is the last module) @@ -124,7 +117,6 @@ def _call_auditor( result = auditor( pr_title=review_context.pr_context.pr_title, summary=review_context.pr_context.summary, - changed_files=audit_files, all_issues=all_issues, ) @@ -133,8 +125,7 @@ def _call_auditor( def forward( self, review_context: ReviewContext, - changed_files: Sequence[ChangedFile], - all_issues: Sequence[Issue], + all_issues: list[Issue], run_id: str | None = None, scopes: list["ScopeResult"] | None = None, topics: list["ScopeResult"] | None = None, @@ -143,7 +134,6 @@ def forward( Args: review_context: ReviewContext containing PR identity (memory loaded per-scope from prior audit episodes) - changed_files: In-scope reviewable files all_issues: All issues found during review run_id: Pipeline run identifier scopes: List of resolved scopes for per-scope episode persistence @@ -173,8 +163,7 @@ def forward( result = self._call_auditor( auditor, review_context, - list(changed_files), - list(all_issues), + all_issues, run_id, scopes, topics, diff --git a/src/codespy/agents/reviewer/reviewer.py b/src/codespy/agents/reviewer/reviewer.py index 6b77982..d8d1440 100644 --- a/src/codespy/agents/reviewer/reviewer.py +++ b/src/codespy/agents/reviewer/reviewer.py @@ -245,14 +245,8 @@ def forward(self, config: ReviewConfig) -> ReviewResult: ) logger.info(f"Found {len(all_issues)} issues") # Step 4: Run Audit (loads own prior episodes per scope, no memory inheritance from parallel modules) - scoped_files = self._collect_scoped_files(scopes) - logger.info( - f"Audit input: {len(scoped_files)} in-scope files " - f"(filtered from {len(pr.changed_files)} total)" - ) quality_assessment, recommendation = self.auditor( review_context=review_ctx, - changed_files=scoped_files, all_issues=all_issues, run_id=run_id, scopes=scopes, @@ -279,29 +273,6 @@ def forward(self, config: ReviewConfig) -> ReviewResult: signature_stats=signature_stats_list, ) - @staticmethod - def _collect_scoped_files(scopes: list) -> list[ChangedFile]: - """Collect de-duplicated changed files from identified scopes. - - The scope identifier already filters out binaries, vendor directories, - lock files, etc. This method collects only the in-scope files so the - summarizer operates on the same focused set as the review modules. - - Args: - scopes: Identified scopes from scope_resolver - - Returns: - De-duplicated list of ChangedFile objects from all scopes - """ - seen: set[str] = set() - scoped_files: list[ChangedFile] = [] - for scope in scopes: - for f in scope.changed_files: - if f.filename not in seen: - seen.add(f.filename) - scoped_files.append(f) - return scoped_files - def _collect_signature_stats(self) -> list[SignatureStatsResult]: """Collect statistics from all signatures that executed. From 8c101f609edfe6963d7add09fd6a8b235c6ab51d Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Tue, 1 Sep 2026 23:54:21 +0200 Subject: [PATCH 2/2] fixes --- CHANGELOG.md | 8 +++ pyproject.toml | 2 +- src/codespy/__init__.py | 2 +- .../agents/reviewer/modules/scope_resolver.py | 49 +++++++++++++------ tests/test_scope_resolver.py | 11 ++++- 5 files changed, 54 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8be7baf..87b612e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +## [1.0.16] - 2026-09-01 + +### Changed +- Manifest discovery (`_discover_manifests`) now scans only ancestor directories of changed files instead of walking the entire repository tree — significant performance improvement for large repos +- `_discover_manifests` signature: added `changed_files: list[ChangedFile]` parameter +- Auditor no longer receives per-file metadata: removed `changed_files` input from `AuditSignature` and `forward()` / `_call_auditor()` parameters +- `Auditor.forward()` signature simplified: `Sequence[ChangedFile]` parameter dropped, `all_issues` type narrowed from `Sequence[Issue]` to `list[Issue]` + ## [1.0.15] - 2026-09-01 ### Changed diff --git a/pyproject.toml b/pyproject.toml index 50c3628..839af9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "codespy-ai" -version = "1.0.15" +version = "1.0.16" description = "Code review agent powered by DSPy" readme = "README.md" license = "MIT" diff --git a/src/codespy/__init__.py b/src/codespy/__init__.py index 47ad5af..7c3e0be 100644 --- a/src/codespy/__init__.py +++ b/src/codespy/__init__.py @@ -1,3 +1,3 @@ """codespy - Code review agent powered by DSPy.""" -__version__ = "1.0.15" +__version__ = "1.0.16" diff --git a/src/codespy/agents/reviewer/modules/scope_resolver.py b/src/codespy/agents/reviewer/modules/scope_resolver.py index b4bb4e5..d49a60a 100644 --- a/src/codespy/agents/reviewer/modules/scope_resolver.py +++ b/src/codespy/agents/reviewer/modules/scope_resolver.py @@ -624,7 +624,7 @@ def _resolve( Tuple of (active scopes, orphan files) """ excluded_dirs = self._settings.excluded_directories - manifests = self._discover_manifests(repo_path, excluded_dirs) + manifests = self._discover_manifests(repo_path, changed_files, excluded_dirs) logger.info( "Manifest discovery at %s found %d manifest(s): %s", repo_path, @@ -701,39 +701,60 @@ def _resolve( return active_scopes, orphans def _discover_manifests( - self, repo_path: Path, excluded_dirs: list[str] + self, repo_path: Path, changed_files: list[ChangedFile], excluded_dirs: list[str] ) -> dict[Path, tuple[str, str]]: - """Discover all package manifests in the repo. + """Discover package manifests in ancestor directories of changed files. Args: repo_path: Path to the repository root + changed_files: List of changed files to derive ancestor directories from excluded_dirs: List of directory names to exclude from scanning Returns: Dict mapping manifest directory -> (package manager, filename) """ - logger.debug("Walking %s for manifests (excluded: %s)", repo_path, excluded_dirs) + logger.debug("Scanning ancestor directories for manifests (excluded: %s)", excluded_dirs) manifests: dict[Path, tuple[str, str]] = {} excluded_set = set(excluded_dirs) - for root, dirs, files in os.walk(repo_path): - # Skip excluded and hidden directories - dirs[:] = [d for d in dirs if d not in excluded_set and not d.startswith(".")] + # Collect all unique ancestor directories from changed files + ancestor_dirs: set[Path] = set() + ancestor_dirs.add(Path(".")) # Always include root + + for changed_file in changed_files: + parts = changed_file.filename.split("/") + # Build each ancestor prefix from the path + for depth in range(1, len(parts)): + ancestor = Path("/".join(parts[:depth])) + ancestor_dirs.add(ancestor) + + # Scan each ancestor directory for manifests + for rel_dir in ancestor_dirs: + # Skip if any path component is in excluded_dirs or starts with . (but not root ".") + if rel_dir != Path("."): + dir_name = str(rel_dir) + if any(part in excluded_set or part.startswith(".") for part in dir_name.split("/")): + continue + + dir_path = repo_path / rel_dir + if not dir_path.is_dir(): + continue + + try: + files = os.listdir(dir_path) + except OSError: + continue for filename in files: # Check exact matches if filename in MANIFEST_FILES: - manifest_path = Path(root) / filename - rel_path = manifest_path.relative_to(repo_path) - manifests[rel_path.parent] = (MANIFEST_FILES[filename], filename) - continue + manifests[rel_dir] = (MANIFEST_FILES[filename], filename) + break # Check glob patterns for pattern, pkg_mgr in MANIFEST_GLOBS.items(): if fnmatch.fnmatch(filename, pattern): - manifest_path = Path(root) / filename - rel_path = manifest_path.relative_to(repo_path) - manifests[rel_path.parent] = (pkg_mgr, filename) + manifests[rel_dir] = (pkg_mgr, filename) break return manifests diff --git a/tests/test_scope_resolver.py b/tests/test_scope_resolver.py index 6255d2f..58e62d0 100644 --- a/tests/test_scope_resolver.py +++ b/tests/test_scope_resolver.py @@ -72,8 +72,13 @@ def test_discover_manifests(self): (repo_path / "services" / "api").mkdir(parents=True) (repo_path / "services" / "api" / "go.mod").touch() + changed_files = [ + ChangedFile(filename="packages/auth/index.ts", status=FileStatus.MODIFIED), + ChangedFile(filename="services/api/main.go", status=FileStatus.MODIFIED), + ] + resolver = ScopeResolver() - manifests = resolver._discover_manifests(repo_path, []) + manifests = resolver._discover_manifests(repo_path, changed_files, []) assert len(manifests) == 2 assert Path("packages/auth") in manifests @@ -268,14 +273,16 @@ def test_root_does_not_suppress_when_nested_manifests_exist(self): (repo_path / "scripts" / "deploy").mkdir(parents=True) changed_files = [ + ChangedFile(filename="packages/auth/src/index.ts", status=FileStatus.MODIFIED), ChangedFile(filename="scripts/deploy/prod.sh", status=FileStatus.MODIFIED), ] resolver = ScopeResolver() scopes, orphans = resolver._resolve(repo_path, changed_files, "owner/repo") - # scripts/deploy indicator should fire — root has nested manifests + # Both packages/auth (manifest scope) and scripts/deploy (indicator scope) should exist scope_subroots = [s.subroot for s in scopes] + assert "packages/auth" in scope_subroots assert "scripts/deploy" in scope_subroots