diff --git a/CHANGELOG.md b/CHANGELOG.md index a202b43..38d6985 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ ## [Unreleased] +## [1.2.2] - 2026-09-16 + +### Changed +- **BREAKING**: Codebase restructured to separate agents from workflow orchestration + - Review agents moved from `codespy.agents.reviewer.modules.*` to `codespy.agents.review.*` subpackages (audit, code_review, doc, scope, summary, supply_chain) + - Workflow orchestration moved from `codespy.agents.reviewer` to `codespy.workflows.review` (pipeline, reporters, server, workflow-owned models) + - Agent-owned types (`Issue`, `PRContext`, `ReviewContext`, `ReviewMetadata`, `IssueCategory`, `IssueSeverity`) moved to `codespy.agents.review.models` + - Scope-specific types (`ScopeResult`, `ScopeType`, `PackageManifest`) moved to `codespy.agents.review.scope.models` + - Workflow-owned types (`ReviewResult`, `ReviewConfig`, `RemoteReviewConfig`, `LocalReviewConfig`, `SignatureStatsResult`) moved to `codespy.workflows.review.models` + - `helpers.py` moved from `codespy.agents.reviewer.modules` to `codespy.agents.review` + - `doc_extractor.py` moved from `codespy.agents.reviewer.modules` to `codespy.agents.review.doc` + - `manifest_parser.py` moved from `codespy.agents.reviewer.modules` to `codespy.agents.review.scope` + - MCP server moved from `codespy.agents.reviewer.server` to `codespy.workflows.review.server` + - CLI modules (`cli_local`, `cli_remote`, `cli_mcp_server`) updated for new import paths + - All tests updated for new import paths + +### Removed +- `codespy.agents.reviewer` package (replaced by `codespy.agents.review` + `codespy.workflows.review`) + ## [1.2.1] - 2026-09-14 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index a2038a4..623be61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "codespy-ai" -version = "1.2.1" +version = "1.2.2" 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 855842b..c6c0813 100644 --- a/src/codespy/__init__.py +++ b/src/codespy/__init__.py @@ -1,3 +1,3 @@ """codespy - Code review agent powered by DSPy.""" -__version__ = "1.2.1" +__version__ = "1.2.2" diff --git a/src/codespy/agents/review/__init__.py b/src/codespy/agents/review/__init__.py new file mode 100644 index 0000000..bcc89d8 --- /dev/null +++ b/src/codespy/agents/review/__init__.py @@ -0,0 +1,6 @@ +from codespy.agents.review.audit import Auditor +from codespy.agents.review.code_review import CodeReviewer +from codespy.agents.review.doc import DocReviewer +from codespy.agents.review.scope import ScopeResolver +from codespy.agents.review.summary import Summarizer +from codespy.agents.review.supply_chain import SupplyChainAuditor diff --git a/src/codespy/agents/review/audit/__init__.py b/src/codespy/agents/review/audit/__init__.py new file mode 100644 index 0000000..da49133 --- /dev/null +++ b/src/codespy/agents/review/audit/__init__.py @@ -0,0 +1 @@ +from codespy.agents.review.audit.agent import Auditor diff --git a/src/codespy/agents/reviewer/modules/auditor.py b/src/codespy/agents/review/audit/agent.py similarity index 96% rename from src/codespy/agents/reviewer/modules/auditor.py rename to src/codespy/agents/review/audit/agent.py index 0d7adf7..2f1d0b6 100644 --- a/src/codespy/agents/reviewer/modules/auditor.py +++ b/src/codespy/agents/review/audit/agent.py @@ -8,14 +8,13 @@ from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.context_safe import ContextSafe from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus -from codespy.agents.memory.hippocampus.context_memory import Topic -from codespy.agents.reviewer.models import Issue, ReviewContext -from codespy.agents.reviewer.modules.scope_resolver import _deepest_common_folder +from codespy.agents.review.models import Issue, ReviewContext +from codespy.agents.review.helpers import deepest_common_folder from codespy.config import get_settings from codespy.config_memory import get_episode_store if TYPE_CHECKING: - from codespy.agents.reviewer.models import ScopeResult + from codespy.agents.review.scope.models import ScopeResult logger = logging.getLogger(__name__) diff --git a/src/codespy/agents/review/code_review/__init__.py b/src/codespy/agents/review/code_review/__init__.py new file mode 100644 index 0000000..436d1a4 --- /dev/null +++ b/src/codespy/agents/review/code_review/__init__.py @@ -0,0 +1 @@ +from codespy.agents.review.code_review.agent import CodeReviewer diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/review/code_review/agent.py similarity index 98% rename from src/codespy/agents/reviewer/modules/code_reviewer.py rename to src/codespy/agents/review/code_review/agent.py index 0c95cb2..e98611a 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/review/code_review/agent.py @@ -13,8 +13,9 @@ from codespy.agents.context_safe import ContextSafe from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.memory.hippocampus.episode import submit_episode_save -from codespy.agents.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult -from codespy.agents.reviewer.modules.helpers import ( +from codespy.agents.review.models import Issue, IssueCategory, ReviewContext +from codespy.agents.review.scope.models import ScopeResult +from codespy.agents.review.helpers import ( issues_to_markdown, make_scope_relative, resolve_scope_root, diff --git a/src/codespy/agents/review/doc/__init__.py b/src/codespy/agents/review/doc/__init__.py new file mode 100644 index 0000000..75bfff6 --- /dev/null +++ b/src/codespy/agents/review/doc/__init__.py @@ -0,0 +1 @@ +from codespy.agents.review.doc.agent import DocReviewer diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/review/doc/agent.py similarity index 98% rename from src/codespy/agents/reviewer/modules/doc_reviewer.py rename to src/codespy/agents/review/doc/agent.py index a77cd93..5e12d8a 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/review/doc/agent.py @@ -13,9 +13,10 @@ from codespy.agents.context_safe import ContextSafe from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.memory.hippocampus.episode import submit_episode_save -from codespy.agents.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult -from codespy.agents.reviewer.modules.doc_extractor import extract_documentation -from codespy.agents.reviewer.modules.helpers import ( +from codespy.agents.review.models import Issue, IssueCategory, ReviewContext +from codespy.agents.review.scope.models import ScopeResult +from codespy.agents.review.doc.doc_extractor import extract_documentation +from codespy.agents.review.helpers import ( build_patches, issues_to_markdown, make_scope_relative, diff --git a/src/codespy/agents/reviewer/modules/doc_extractor.py b/src/codespy/agents/review/doc/doc_extractor.py similarity index 100% rename from src/codespy/agents/reviewer/modules/doc_extractor.py rename to src/codespy/agents/review/doc/doc_extractor.py diff --git a/src/codespy/agents/reviewer/modules/helpers.py b/src/codespy/agents/review/helpers.py similarity index 86% rename from src/codespy/agents/reviewer/modules/helpers.py rename to src/codespy/agents/review/helpers.py index 620c766..646acae 100644 --- a/src/codespy/agents/reviewer/modules/helpers.py +++ b/src/codespy/agents/review/helpers.py @@ -8,11 +8,11 @@ from pathlib import Path from typing import TYPE_CHECKING -from codespy.agents.reviewer.models import Issue +from codespy.agents.review.models import Issue from codespy.tools.git.models import ChangedFile if TYPE_CHECKING: - from codespy.agents.reviewer.models import ScopeResult + from codespy.agents.review.scope.models import ScopeResult, PackageManifest logger = logging.getLogger(__name__) @@ -102,7 +102,29 @@ def resolve_scope_root(repo_path: Path, subroot: str) -> Path: return repo_path if subroot == "." else repo_path / subroot -def make_scope_relative(scope: ScopeResult) -> ScopeResult: +def deepest_common_folder(scopes: list["ScopeResult"], repo_slug: str) -> str: + """Compute the deepest common ancestor directory across all scope subroots. + + Args: + scopes: List of scope results + repo_slug: Repository slug for fallback path + + Returns: + Deepest common ancestor path (e.g., "/repo/scope/subroot/") + """ + subroots = [s.subroot for s in scopes] + if not subroots or any(sr in (".", "") for sr in subroots): + return f"/{repo_slug}/" + try: + common = os.path.commonpath(subroots) + except ValueError: + common = "" + if not common or common == ".": + return f"/{repo_slug}/" + return f"/{repo_slug}/{common.strip('/')}/" + + +def make_scope_relative(scope: "ScopeResult") -> "ScopeResult": """Create a copy of a ScopeResult with file paths relative to scope.subroot. When MCP tools are rooted at repo_path/scope.subroot, the agent needs file @@ -116,7 +138,7 @@ def make_scope_relative(scope: ScopeResult) -> ScopeResult: New ScopeResult with scope-relative file paths in changed_files. The subroot is set to "." since paths are now relative to it. """ - from codespy.agents.reviewer.models import PackageManifest, ScopeResult + from codespy.agents.review.scope.models import ScopeResult, PackageManifest if scope.subroot == ".": return scope # Already at repo root, no transformation needed @@ -145,6 +167,7 @@ def make_scope_relative(scope: ScopeResult) -> ScopeResult: ), package_manager=scope.package_manifest.package_manager, dependencies_changed=scope.package_manifest.dependencies_changed, + package_name=scope.package_manifest.package_name, ) return ScopeResult( repo=scope.repo, @@ -156,6 +179,7 @@ def make_scope_relative(scope: ScopeResult) -> ScopeResult: changed_files=relative_files, reason=scope.reason, skills=scope.skills, + description=scope.description, ) diff --git a/src/codespy/agents/review/models.py b/src/codespy/agents/review/models.py new file mode 100644 index 0000000..b8e7b08 --- /dev/null +++ b/src/codespy/agents/review/models.py @@ -0,0 +1,138 @@ +"""Cross-cutting review types.""" + +from __future__ import annotations + +from enum import StrEnum +from pathlib import Path +from typing import TYPE_CHECKING + +from pydantic import BaseModel, Field + +from codespy.agents.memory.hippocampus import ContextMemory +from codespy.tools.git.models import PullRequest + +if TYPE_CHECKING: + from codespy.agents.memory.hippocampus.context_memory import Topic + + +class IssueSeverity(StrEnum): + """Severity level of an issue.""" + + CRITICAL = "critical" + HIGH = "high" + MEDIUM = "medium" + LOW = "low" + INFO = "info" + + +class IssueCategory(StrEnum): + """Category of an issue.""" + + SECURITY = "security" + BUG = "bug" + DOCUMENTATION = "documentation" + SMELL = "smell" + + +class PRContext(BaseModel): + """Shared PR identity passed to all review modules after summarization. + + Built by the pipeline orchestrator after the Summarizer runs, then + threaded through scope identification, review modules, and audit. + Each module constructs its own Hippocampus question from these fields. + """ + + repo_slug: str = Field( + description="Host-qualified repo identifier (e.g. github.com/owner/repo)" + ) + pr_number: int = Field(description="PR number") + pr_title: str = Field(description="PR title") + pr_url: str = Field(description="Full PR URL (e.g. https://github.com/owner/repo/pull/123)") + pr_description: str = Field(default="", description="PR body/description") + summary: str = Field(description="2-3 sentence PR summary produced by Summarizer") + + @property + def repo_full_name(self) -> str: + """Get owner/repo from host-qualified repo_slug.""" + # repo_slug is "github.com/owner/repo" or just "owner/repo" + parts = self.repo_slug.split("/") + if len(parts) >= 3: + return "/".join(parts[1:]) # strip host + return self.repo_slug + + def to_topic(self) -> "Topic": + """Build a Topic representing this PR. + + Returns: + Topic object with id as PR URL, type as "pull_request", and description as "PR #N: Title" + """ + from codespy.agents.memory.hippocampus.context_memory import Topic + return Topic( + id=self.pr_url, + type="pull_request", + description=f"PR #{self.pr_number}: {self.pr_title}"[:500], + ) + + +class ReviewMetadata(BaseModel): + """Runtime pipeline state, stable once constructed at pipeline start. + + Groups repo_path, run_id, pr, and is_local to reduce parameter + proliferation across module method signatures. + """ + + repo_path: Path + run_id: str | None = None + pr: PullRequest | None = None + is_local: bool = False + + +class ReviewContext(BaseModel): + """Evolving pipeline state threaded through review stages. + + Carries the immutable PR identity and runtime pipeline metadata. + Context memory is loaded independently by each module from its own prior episodes. + """ + + pr_context: PRContext = Field( + description="Immutable PR identity (repo, number, title, summary)" + ) + memory: ContextMemory | None = Field( + default=None, description="Unused — each module loads its own prior episodes. Kept for API compatibility." + ) + metadata: ReviewMetadata | None = Field(default=None, description="Runtime pipeline state") + + +class Issue(BaseModel): + """Represents a single issue found during review.""" + + category: IssueCategory = Field(description="Issue category") + severity: IssueSeverity = Field( + default=IssueSeverity.MEDIUM, + description="Issue severity: critical, high, medium, low, or info", + ) + title: str = Field(description="Brief title of the issue") + description: str = Field(description="≤25 word imperative description. No filler.") + filename: str = Field(description="File where the issue was found") + line_start: int | None = Field(default=None, description="Starting line number") + line_end: int | None = Field(default=None, description="Ending line number") + code_snippet: str | None = Field( + default=None, description="Deprecated—use line numbers. Leave None." + ) + suggestion: str | None = Field(default=None, description="Suggested fix or improvement") + cwe_id: str | None = Field( + default=None, description="CWE ID for security issues (e.g., CWE-79)" + ) + confidence: float = Field(default=0.8, ge=0.0, le=1.0, description="Confidence score (0-1)") + + @property + def location(self) -> str: + """Get a human-readable location string.""" + if self.line_start: + if self.line_end and self.line_end != self.line_start: + return f"{self.filename}:{self.line_start}-{self.line_end}" + return f"{self.filename}:{self.line_start}" + return self.filename + + + diff --git a/src/codespy/agents/review/scope/__init__.py b/src/codespy/agents/review/scope/__init__.py new file mode 100644 index 0000000..04cab8e --- /dev/null +++ b/src/codespy/agents/review/scope/__init__.py @@ -0,0 +1,2 @@ +from codespy.agents.review.scope.agent import MANIFEST_FILES, MANIFEST_GLOBS, ScopeResolver, derive_sparse_paths +from codespy.agents.review.scope.models import PackageManifest, ScopeResult, ScopeType diff --git a/src/codespy/agents/reviewer/modules/scope_resolver.py b/src/codespy/agents/review/scope/agent.py similarity index 97% rename from src/codespy/agents/reviewer/modules/scope_resolver.py rename to src/codespy/agents/review/scope/agent.py index c307d00..a8c42d9 100644 --- a/src/codespy/agents/reviewer/modules/scope_resolver.py +++ b/src/codespy/agents/review/scope/agent.py @@ -21,13 +21,9 @@ from codespy.agents.context_safe import ContextSafe from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.memory.hippocampus.episode import submit_episode_save -from codespy.agents.reviewer.models import ( - PackageManifest, - ReviewContext, - ScopeResult, - ScopeType, -) -from codespy.agents.reviewer.modules.manifest_parser import extract_package_name +from codespy.agents.review.models import ReviewContext +from codespy.agents.review.scope.manifest_parser import extract_package_name +from codespy.agents.review.scope.models import PackageManifest, ScopeResult, ScopeType from codespy.config import get_settings from codespy.config_memory import get_episode_store from codespy.tools.git.client import get_client @@ -87,28 +83,6 @@ def collect_skills(repo_path: Path, subroot: str) -> str | None: return "\n\n".join(sections) if sections else None -def _deepest_common_folder(scopes: list[ScopeResult], repo_slug: str) -> str: - """Compute the deepest common ancestor directory across all scope subroots. - - Args: - scopes: List of scope results - repo_slug: Repository slug for fallback path - - Returns: - Deepest common ancestor path (e.g., "/repo/scope/subroot/") - """ - subroots = [s.subroot for s in scopes] - if not subroots or any(sr in (".", "") for sr in subroots): - return f"/{repo_slug}/" - try: - common = os.path.commonpath(subroots) - except ValueError: - common = "" - if not common or common == ".": - return f"/{repo_slug}/" - return f"/{repo_slug}/{common.strip('/')}/" - - # Exact filename matches -> package manager MANIFEST_FILES: dict[str, str] = { # Go @@ -556,7 +530,7 @@ async def _ensure_repo(self, pr: PullRequest, repo_path: Path, is_local: bool) - ) logger.info("Clone complete: %s", repo_path) - # Ensure manifest files are present at root and parent directories + # Ensure manifest files at root and parent directories are checked out await self._ensure_manifests(repo_path, changed_file_paths) async def _ensure_manifests(self, repo_path: Path, changed_files: list[str]) -> None: diff --git a/src/codespy/agents/reviewer/modules/manifest_parser.py b/src/codespy/agents/review/scope/manifest_parser.py similarity index 100% rename from src/codespy/agents/reviewer/modules/manifest_parser.py rename to src/codespy/agents/review/scope/manifest_parser.py diff --git a/src/codespy/agents/review/scope/models.py b/src/codespy/agents/review/scope/models.py new file mode 100644 index 0000000..dc817cb --- /dev/null +++ b/src/codespy/agents/review/scope/models.py @@ -0,0 +1,91 @@ +"""Scope-agent-owned types.""" + +from __future__ import annotations + +from enum import StrEnum +from typing import TYPE_CHECKING + +from pydantic import BaseModel, Field + +from codespy.tools.git.models import ChangedFile + +if TYPE_CHECKING: + from codespy.agents.memory.hippocampus.context_memory import Topic + + +class ScopeType(StrEnum): + """Type of code scope in a repository.""" + + LIBRARY = "library" # Shared code that others import + SERVICE = "service" # Isolated microservice with explicit APIs + APPLICATION = "application" # Standalone app or frontend + SCRIPT = "script" # Build/deployment scripts, tooling + + +class PackageManifest(BaseModel): + """Package management file information for a scope.""" + + manifest_path: str = Field(description="Path to manifest file (e.g., package.json)") + lock_file_path: str | None = Field( + default=None, description="Path to lock file (e.g., package-lock.json)" + ) + package_manager: str = Field(description="Package manager name (e.g., npm, go, pip)") + dependencies_changed: bool = Field( + default=False, description="Whether PR modified this manifest or lock file" + ) + package_name: str | None = Field(default=None, description="Package identity from manifest") + + +class ScopeResult(BaseModel): + """A detected scope/subroot in the repository.""" + + repo: str = Field( + default="", description="Repo identifier: 'owner/repo' (remote) or local dir name" + ) + subroot: str = Field(description="Path relative to repo root (e.g., packages/auth)") + scope_type: ScopeType = Field(description="Type of scope (library, service, etc.)") + has_changes: bool = Field( + default=False, description="Whether this scope has changed files from PR" + ) + + language: str | None = Field(default=None, description="Primary language detected") + package_manifest: PackageManifest | None = Field( + default=None, description="Package manifest info if present" + ) + changed_files: list[ChangedFile] = Field( + default_factory=list, description="Changed files belonging to this scope" + ) + reason: str = Field(description="Explanation for why this scope was identified") + skills: str | None = Field( + default=None, description="Project/scope instructions inherited from ancestor directories" + ) + description: str = Field( + default="", description="Description of scope's role in the project (max 500 chars)" + ) + + model_config = {"arbitrary_types_allowed": True} + + def scope_path(self) -> str: + """Return the storage-relative path for this scope. + + Used by Hippocampus memory as the base directory for episode files. + ``subroot == "."`` (repo root) results in ``/{repo}/``. + """ + if self.subroot in (".", ""): + return f"/{self.repo}/" + return f"/{self.repo}/{self.subroot.strip('/')}/" + + def topic(self, repo_full_name: str) -> "Topic": + """Build the Topic for this scope. + + Args: + repo_full_name: Repository full name (owner/repo) + + Returns: + Topic object with id and description + """ + from codespy.agents.memory.hippocampus.context_memory import Topic, make_topic_id + + package_name = self.package_manifest.package_name if self.package_manifest else None + topic_id = make_topic_id(repo_full_name, self.subroot, package_name) + return Topic(id=topic_id, type="project_scope", description=self.description) diff --git a/src/codespy/agents/review/summary/__init__.py b/src/codespy/agents/review/summary/__init__.py new file mode 100644 index 0000000..719aa03 --- /dev/null +++ b/src/codespy/agents/review/summary/__init__.py @@ -0,0 +1 @@ +from codespy.agents.review.summary.agent import Summarizer diff --git a/src/codespy/agents/reviewer/modules/summarizer.py b/src/codespy/agents/review/summary/agent.py similarity index 97% rename from src/codespy/agents/reviewer/modules/summarizer.py rename to src/codespy/agents/review/summary/agent.py index ebe8a13..1bfd75a 100644 --- a/src/codespy/agents/reviewer/modules/summarizer.py +++ b/src/codespy/agents/review/summary/agent.py @@ -10,12 +10,13 @@ from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.memory.hippocampus.episode import submit_episode_save from codespy.agents.memory.hippocampus.context_memory import Topic -from codespy.agents.reviewer.modules.scope_resolver import _deepest_common_folder +from codespy.agents.review.helpers import deepest_common_folder from codespy.config import get_settings from codespy.config_memory import get_episode_store if TYPE_CHECKING: - from codespy.agents.reviewer.models import PRContext, ScopeResult + from codespy.agents.review.models import PRContext + from codespy.agents.review.scope.models import ScopeResult logger = logging.getLogger(__name__) diff --git a/src/codespy/agents/review/supply_chain/__init__.py b/src/codespy/agents/review/supply_chain/__init__.py new file mode 100644 index 0000000..2811693 --- /dev/null +++ b/src/codespy/agents/review/supply_chain/__init__.py @@ -0,0 +1 @@ +from codespy.agents.review.supply_chain.agent import SupplyChainAuditor diff --git a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py b/src/codespy/agents/review/supply_chain/agent.py similarity index 98% rename from src/codespy/agents/reviewer/modules/supply_chain_auditor.py rename to src/codespy/agents/review/supply_chain/agent.py index 8a66e9a..54da8e8 100644 --- a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +++ b/src/codespy/agents/review/supply_chain/agent.py @@ -13,8 +13,9 @@ from codespy.agents.context_safe import ContextSafe from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.memory.hippocampus.episode import submit_episode_save -from codespy.agents.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult -from codespy.agents.reviewer.modules.helpers import ( +from codespy.agents.review.models import Issue, IssueCategory, ReviewContext +from codespy.agents.review.scope.models import ScopeResult +from codespy.agents.review.helpers import ( issues_to_markdown, resolve_scope_root, restore_repo_paths, @@ -67,7 +68,7 @@ class SupplyChainSecuritySignature(dspy.Signature): - description: ≤25 words, imperative tone, no filler ("Fix X", "Pin Y"). - No polite or conversational language ("I suggest", "Please consider", "Great"). - Do not populate code_snippet—use line numbers instead. - - File paths in issues must be relative to the scope root. + - File paths in issues must be relative to scope root. ## 2. DEPENDENCY SECURITY (package manifests) diff --git a/src/codespy/agents/reviewer/__init__.py b/src/codespy/agents/reviewer/__init__.py deleted file mode 100644 index d57ea91..0000000 --- a/src/codespy/agents/reviewer/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Reviewer agent - AI-powered code review.""" - -from codespy.agents.reviewer.models import ( - Issue, - IssueCategory, - IssueSeverity, - LocalReviewConfig, - RemoteReviewConfig, - ReviewConfig, - ReviewResult, -) -from codespy.agents.reviewer.reviewer import ReviewPipeline - -__all__ = [ - "ReviewPipeline", - "ReviewResult", - "ReviewConfig", - "RemoteReviewConfig", - "LocalReviewConfig", - "Issue", - "IssueCategory", - "IssueSeverity", -] diff --git a/src/codespy/agents/reviewer/modules/__init__.py b/src/codespy/agents/reviewer/modules/__init__.py deleted file mode 100644 index b0ebcaa..0000000 --- a/src/codespy/agents/reviewer/modules/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -"""DSPy modules for code review.""" - -from codespy.agents.reviewer.modules.auditor import Auditor -from codespy.agents.reviewer.modules.code_reviewer import CodeReviewer -from codespy.agents.reviewer.modules.doc_reviewer import DocReviewer -from codespy.agents.reviewer.modules.scope_resolver import ScopeResolver -from codespy.agents.reviewer.modules.summarizer import Summarizer -from codespy.agents.reviewer.modules.supply_chain_auditor import SupplyChainAuditor - -__all__ = [ - "Auditor", - "CodeReviewer", - "DocReviewer", - "ScopeResolver", - "Summarizer", - "SupplyChainAuditor", -] diff --git a/src/codespy/agents/reviewer/reporters/__init__.py b/src/codespy/agents/reviewer/reporters/__init__.py deleted file mode 100644 index f1eaf7b..0000000 --- a/src/codespy/agents/reviewer/reporters/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Reporters for review output.""" - -from codespy.agents.reviewer.reporters.base import BaseReporter -from codespy.agents.reviewer.reporters.git import GitReporter -from codespy.agents.reviewer.reporters.stdout import StdoutReporter - -__all__ = [ - "BaseReporter", - "StdoutReporter", - "GitReporter", -] diff --git a/src/codespy/cli_local.py b/src/codespy/cli_local.py index 22caa0b..60cdd43 100644 --- a/src/codespy/cli_local.py +++ b/src/codespy/cli_local.py @@ -107,8 +107,8 @@ def review_local( ) try: - from codespy.agents.reviewer.models import LocalReviewConfig - from codespy.agents.reviewer.reviewer import ReviewPipeline + from codespy.workflows.review.models import LocalReviewConfig + from codespy.workflows.review.pipeline import ReviewPipeline pipeline = ReviewPipeline(settings) @@ -129,7 +129,7 @@ def review_local( ) ) - from codespy.agents.reviewer.reporters import StdoutReporter + from codespy.workflows.review.reporters import StdoutReporter stdout_reporter = StdoutReporter(format=settings.output_format, console=console) stdout_reporter.report(result) @@ -220,8 +220,8 @@ def review_uncommitted( ) try: - from codespy.agents.reviewer.models import LocalReviewConfig - from codespy.agents.reviewer.reviewer import ReviewPipeline + from codespy.workflows.review.models import LocalReviewConfig + from codespy.workflows.review.pipeline import ReviewPipeline pipeline = ReviewPipeline(settings) @@ -242,7 +242,7 @@ def review_uncommitted( ) ) - from codespy.agents.reviewer.reporters import StdoutReporter + from codespy.workflows.review.reporters import StdoutReporter stdout_reporter = StdoutReporter(format=settings.output_format, console=console) stdout_reporter.report(result) diff --git a/src/codespy/cli_mcp_server.py b/src/codespy/cli_mcp_server.py index 1081a36..ef9902a 100644 --- a/src/codespy/cli_mcp_server.py +++ b/src/codespy/cli_mcp_server.py @@ -52,6 +52,6 @@ def serve( console.print(f"[red]Error:[/red] {e}") raise typer.Exit(1) from None - from codespy.agents.reviewer.server import run_server + from codespy.workflows.review.server import run_server run_server(settings=settings) diff --git a/src/codespy/cli_remote.py b/src/codespy/cli_remote.py index d7d4d29..2761358 100644 --- a/src/codespy/cli_remote.py +++ b/src/codespy/cli_remote.py @@ -159,8 +159,8 @@ def review( ) try: - from codespy.agents.reviewer.models import RemoteReviewConfig - from codespy.agents.reviewer.reviewer import ReviewPipeline + from codespy.workflows.review.models import RemoteReviewConfig + from codespy.workflows.review.pipeline import ReviewPipeline pipeline = ReviewPipeline(settings) @@ -183,7 +183,7 @@ def review( ) # Output results using reporters - from codespy.agents.reviewer.reporters import GitReporter, StdoutReporter + from codespy.workflows.review.reporters import GitReporter, StdoutReporter if settings.output_stdout: stdout_reporter = StdoutReporter(format=settings.output_format, console=console) diff --git a/src/codespy/tools/git/patch_utils.py b/src/codespy/tools/git/patch_utils.py index 0ae3851..2af0e4d 100644 --- a/src/codespy/tools/git/patch_utils.py +++ b/src/codespy/tools/git/patch_utils.py @@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any if TYPE_CHECKING: - from codespy.agents.reviewer.models import ScopeResult + from codespy.agents.review.scope.models import ScopeResult from codespy.tools.git.models import ChangedFile, FileStatus from codespy.tools.parsers.treesitter import FunctionInfo, TreeSitterParser diff --git a/src/codespy/workflows/__init__.py b/src/codespy/workflows/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/codespy/workflows/review/__init__.py b/src/codespy/workflows/review/__init__.py new file mode 100644 index 0000000..a91cca5 --- /dev/null +++ b/src/codespy/workflows/review/__init__.py @@ -0,0 +1,7 @@ +from codespy.workflows.review.models import ( + LocalReviewConfig, + RemoteReviewConfig, + ReviewConfig, + ReviewResult, +) +from codespy.workflows.review.pipeline import ReviewPipeline diff --git a/src/codespy/agents/reviewer/models.py b/src/codespy/workflows/review/models.py similarity index 53% rename from src/codespy/agents/reviewer/models.py rename to src/codespy/workflows/review/models.py index dbc34c5..187b78e 100644 --- a/src/codespy/agents/reviewer/models.py +++ b/src/codespy/workflows/review/models.py @@ -1,217 +1,13 @@ -"""Data models for code review results.""" +"""Workflow-owned types.""" from __future__ import annotations from datetime import UTC, datetime -from enum import StrEnum from pathlib import Path -from typing import TYPE_CHECKING from pydantic import BaseModel, Field -from codespy.agents.memory.hippocampus import ContextMemory -from codespy.tools.git.models import ChangedFile, PullRequest - -if TYPE_CHECKING: - from codespy.agents.memory.hippocampus.context_memory import Topic - - -class PRContext(BaseModel): - """Shared PR identity passed to all review modules after summarization. - - Built by the pipeline orchestrator after the Summarizer runs, then - threaded through scope identification, review modules, and audit. - Each module constructs its own Hippocampus question from these fields. - """ - - repo_slug: str = Field( - description="Host-qualified repo identifier (e.g. github.com/owner/repo)" - ) - pr_number: int = Field(description="PR number") - pr_title: str = Field(description="PR title") - pr_url: str = Field(description="Full PR URL (e.g. https://github.com/owner/repo/pull/123)") - pr_description: str = Field(default="", description="PR body/description") - summary: str = Field(description="2-3 sentence PR summary produced by Summarizer") - - @property - def repo_full_name(self) -> str: - """Get owner/repo from host-qualified repo_slug.""" - # repo_slug is "github.com/owner/repo" or just "owner/repo" - parts = self.repo_slug.split("/") - if len(parts) >= 3: - return "/".join(parts[1:]) # strip host - return self.repo_slug - - def to_topic(self) -> "Topic": - """Build a Topic representing this PR. - - Returns: - Topic object with id as PR URL, type as "pull_request", and description as "PR #N: Title" - """ - from codespy.agents.memory.hippocampus.context_memory import Topic - return Topic( - id=self.pr_url, - type="pull_request", - description=f"PR #{self.pr_number}: {self.pr_title}"[:500], - ) - - -class IssueSeverity(StrEnum): - """Severity level of an issue.""" - - CRITICAL = "critical" - HIGH = "high" - MEDIUM = "medium" - LOW = "low" - INFO = "info" - - -class IssueCategory(StrEnum): - """Category of an issue.""" - - SECURITY = "security" - BUG = "bug" - DOCUMENTATION = "documentation" - SMELL = "smell" - - -class ScopeType(StrEnum): - """Type of code scope in a repository.""" - - LIBRARY = "library" # Shared code that others import - SERVICE = "service" # Isolated microservice with explicit APIs - APPLICATION = "application" # Standalone app or frontend - SCRIPT = "script" # Build/deployment scripts, tooling - - -class PackageManifest(BaseModel): - """Package management file information for a scope.""" - - manifest_path: str = Field(description="Path to manifest file (e.g., package.json)") - lock_file_path: str | None = Field( - default=None, description="Path to lock file (e.g., package-lock.json)" - ) - package_manager: str = Field(description="Package manager name (e.g., npm, go, pip)") - dependencies_changed: bool = Field( - default=False, description="Whether PR modified this manifest or lock file" - ) - package_name: str | None = Field(default=None, description="Package identity from manifest") - - -class ReviewMetadata(BaseModel): - """Runtime pipeline state, stable once constructed at pipeline start. - - Groups repo_path, run_id, pr, and is_local to reduce parameter - proliferation across module method signatures. - """ - - repo_path: Path - run_id: str | None = None - pr: PullRequest | None = None - is_local: bool = False - - -class ReviewContext(BaseModel): - """Evolving pipeline state threaded through review stages. - - Carries the immutable PR identity and runtime pipeline metadata. - Context memory is loaded independently by each module from its own prior episodes. - """ - - pr_context: PRContext = Field( - description="Immutable PR identity (repo, number, title, summary)" - ) - memory: ContextMemory | None = Field( - default=None, description="Unused — each module loads its own prior episodes. Kept for API compatibility." - ) - metadata: ReviewMetadata | None = Field(default=None, description="Runtime pipeline state") - - -class ScopeResult(BaseModel): - """A detected scope/subroot in the repository.""" - - repo: str = Field( - default="", description="Repo identifier: 'owner/repo' (remote) or local dir name" - ) - subroot: str = Field(description="Path relative to repo root (e.g., packages/auth)") - scope_type: ScopeType = Field(description="Type of scope (library, service, etc.)") - has_changes: bool = Field( - default=False, description="Whether this scope has changed files from PR" - ) - - language: str | None = Field(default=None, description="Primary language detected") - package_manifest: PackageManifest | None = Field( - default=None, description="Package manifest info if present" - ) - changed_files: list[ChangedFile] = Field( - default_factory=list, description="Changed files belonging to this scope" - ) - reason: str = Field(description="Explanation for why this scope was identified") - skills: str | None = Field( - default=None, description="Project/scope instructions inherited from ancestor directories" - ) - description: str = Field( - default="", description="Description of scope's role in the project (max 500 chars)" - ) - - model_config = {"arbitrary_types_allowed": True} - - def scope_path(self) -> str: - """Return the storage-relative path for this scope. - - Used by Hippocampus memory as the base directory for episode files. - ``subroot == "."`` (repo root) results in ``/{repo}/``. - """ - if self.subroot in (".", ""): - return f"/{self.repo}/" - return f"/{self.repo}/{self.subroot.strip('/')}/" - - def topic(self, repo_full_name: str) -> Topic: - """Build the Topic for this scope. - - Args: - repo_full_name: Repository full name (owner/repo) - - Returns: - Topic object with id and description - """ - from codespy.agents.memory.hippocampus.context_memory import Topic, make_topic_id - - package_name = self.package_manifest.package_name if self.package_manifest else None - topic_id = make_topic_id(repo_full_name, self.subroot, package_name) - return Topic(id=topic_id, type="project_scope", description=self.description) - - -class Issue(BaseModel): - """Represents a single issue found during review.""" - - category: IssueCategory = Field(description="Issue category") - severity: IssueSeverity = Field( - default=IssueSeverity.MEDIUM, - description="Issue severity: critical, high, medium, low, or info", - ) - title: str = Field(description="Brief title of the issue") - description: str = Field(description="≤25 word imperative description. No filler.") - filename: str = Field(description="File where the issue was found") - line_start: int | None = Field(default=None, description="Starting line number") - line_end: int | None = Field(default=None, description="Ending line number") - code_snippet: str | None = Field( - default=None, description="Deprecated—use line numbers. Leave None." - ) - suggestion: str | None = Field(default=None, description="Suggested fix or improvement") - cwe_id: str | None = Field( - default=None, description="CWE ID for security issues (e.g., CWE-79)" - ) - confidence: float = Field(default=0.8, ge=0.0, le=1.0, description="Confidence score (0-1)") - - @property - def location(self) -> str: - """Get a human-readable location string.""" - if self.line_start: - if self.line_end and self.line_end != self.line_start: - return f"{self.filename}:{self.line_start}-{self.line_end}" - return f"{self.filename}:{self.line_start}" - return self.filename +from codespy.agents.review.models import Issue, IssueCategory, IssueSeverity class SignatureStatsResult(BaseModel): @@ -240,6 +36,29 @@ def tokens_per_call(self) -> float: return self.tokens / self.call_count +class RemoteReviewConfig(BaseModel): + """Configuration for reviewing a remote PR/MR from GitHub or GitLab.""" + + url: str = Field(description="URL of the GitHub PR or GitLab MR to review") + + +class LocalReviewConfig(BaseModel): + """Configuration for reviewing local git changes without a remote platform.""" + + repo_path: Path = Field(description="Path to the git repository") + base_ref: str = Field( + default="main", + description="Base git ref to compare against (e.g., 'main', 'develop', 'HEAD~5')", + ) + uncommitted: bool = Field( + default=False, description="If True, review uncommitted changes (working tree vs HEAD)" + ) + + +# Union type for review configuration +ReviewConfig = RemoteReviewConfig | LocalReviewConfig + + class ReviewResult(BaseModel): """Complete review results for a pull request (GitHub PR or GitLab MR).""" @@ -257,7 +76,7 @@ class ReviewResult(BaseModel): description="Review timestamp", ) model_used: str = Field(description="LLM model used for review") - issues: list[Issue] = Field(default_factory=list, description="All issues found during review") + issues: list["Issue"] = Field(default_factory=list, description="All issues found during review") overall_summary: str | None = Field(default=None, description="Overall summary of the PR") quality_assessment: str | None = Field( default=None, description="Overall assessment of code quality" @@ -278,31 +97,31 @@ def total_issues(self) -> int: return len(self.issues) @property - def critical_issues(self) -> list[Issue]: + def critical_issues(self) -> list["Issue"]: """Get all critical issues.""" return [i for i in self.issues if i.severity == IssueSeverity.CRITICAL] @property - def security_issues(self) -> list[Issue]: + def security_issues(self) -> list["Issue"]: """Get all security issues.""" return [i for i in self.issues if i.category == IssueCategory.SECURITY] @property - def bug_issues(self) -> list[Issue]: + def bug_issues(self) -> list["Issue"]: """Get all bug issues.""" return [i for i in self.issues if i.category == IssueCategory.BUG] @property - def documentation_issues(self) -> list[Issue]: + def documentation_issues(self) -> list["Issue"]: """Get all documentation issues.""" return [i for i in self.issues if i.category == IssueCategory.DOCUMENTATION] @property - def smell_issues(self) -> list[Issue]: + def smell_issues(self) -> list["Issue"]: """Get all code smell issues.""" return [i for i in self.issues if i.category == IssueCategory.SMELL] - def issues_by_severity(self) -> dict[IssueSeverity, list[Issue]]: + def issues_by_severity(self) -> dict["IssueSeverity", list["Issue"]]: """Group issues by severity.""" result: dict[IssueSeverity, list[Issue]] = {s: [] for s in IssueSeverity} for issue in self.issues: @@ -450,26 +269,3 @@ def to_markdown(self) -> str: def to_json_dict(self) -> dict: """Convert to a JSON-serializable dictionary.""" return self.model_dump(mode="json") - - -class RemoteReviewConfig(BaseModel): - """Configuration for reviewing a remote PR/MR from GitHub or GitLab.""" - - url: str = Field(description="URL of the GitHub PR or GitLab MR to review") - - -class LocalReviewConfig(BaseModel): - """Configuration for reviewing local git changes without a remote platform.""" - - repo_path: Path = Field(description="Path to the git repository") - base_ref: str = Field( - default="main", - description="Base git ref to compare against (e.g., 'main', 'develop', 'HEAD~5')", - ) - uncommitted: bool = Field( - default=False, description="If True, review uncommitted changes (working tree vs HEAD)" - ) - - -# Union type for review configuration -ReviewConfig = RemoteReviewConfig | LocalReviewConfig diff --git a/src/codespy/agents/reviewer/reviewer.py b/src/codespy/workflows/review/pipeline.py similarity index 97% rename from src/codespy/agents/reviewer/reviewer.py rename to src/codespy/workflows/review/pipeline.py index d8d1440..3de83a2 100644 --- a/src/codespy/agents/reviewer/reviewer.py +++ b/src/codespy/workflows/review/pipeline.py @@ -10,18 +10,8 @@ from codespy.agents import configure_dspy, get_cost_tracker, verify_model_access from codespy.agents.memory.hippocampus.context_memory import Topic -from codespy.agents.reviewer.models import ( - Issue, - LocalReviewConfig, - PRContext, - RemoteReviewConfig, - ReviewConfig, - ReviewContext, - ReviewMetadata, - ReviewResult, - SignatureStatsResult, -) -from codespy.agents.reviewer.modules import ( +from codespy.agents.review.models import Issue, PRContext, ReviewContext, ReviewMetadata +from codespy.agents.review import ( Auditor, CodeReviewer, DocReviewer, @@ -30,13 +20,20 @@ SupplyChainAuditor, ) from codespy.agents.memory.hippocampus.episode import join_episode_saves -from codespy.agents.reviewer.modules.helpers import build_patches -from codespy.agents.reviewer.modules.scope_resolver import MANIFEST_FILES, MANIFEST_GLOBS +from codespy.agents.review.helpers import build_patches +from codespy.agents.review.scope import MANIFEST_FILES, MANIFEST_GLOBS from codespy.config import Settings, get_settings from codespy.config_memory import verify_memory_access from codespy.tools.git import ChangedFile, GitClient, PullRequest, get_client from codespy.tools.git.local_diff import build_pr_from_diff from codespy.tools.git.patch_utils import compact_patches +from codespy.workflows.review.models import ( + LocalReviewConfig, + RemoteReviewConfig, + ReviewConfig, + ReviewResult, + SignatureStatsResult, +) logger = logging.getLogger(__name__) diff --git a/src/codespy/workflows/review/reporters/__init__.py b/src/codespy/workflows/review/reporters/__init__.py new file mode 100644 index 0000000..107b848 --- /dev/null +++ b/src/codespy/workflows/review/reporters/__init__.py @@ -0,0 +1,3 @@ +from codespy.workflows.review.reporters.base import BaseReporter +from codespy.workflows.review.reporters.git import GitReporter +from codespy.workflows.review.reporters.stdout import StdoutReporter diff --git a/src/codespy/agents/reviewer/reporters/base.py b/src/codespy/workflows/review/reporters/base.py similarity index 86% rename from src/codespy/agents/reviewer/reporters/base.py rename to src/codespy/workflows/review/reporters/base.py index 0d3e449..3ee090f 100644 --- a/src/codespy/agents/reviewer/reporters/base.py +++ b/src/codespy/workflows/review/reporters/base.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod -from codespy.agents.reviewer.models import ReviewResult +from codespy.workflows.review.models import ReviewResult class BaseReporter(ABC): diff --git a/src/codespy/agents/reviewer/reporters/git.py b/src/codespy/workflows/review/reporters/git.py similarity index 98% rename from src/codespy/agents/reviewer/reporters/git.py rename to src/codespy/workflows/review/reporters/git.py index 3c3296c..bda5a26 100644 --- a/src/codespy/agents/reviewer/reporters/git.py +++ b/src/codespy/workflows/review/reporters/git.py @@ -3,8 +3,9 @@ import logging from typing import TYPE_CHECKING -from codespy.agents.reviewer.models import Issue, IssueSeverity, ReviewResult -from codespy.agents.reviewer.reporters.base import BaseReporter +from codespy.agents.review.models import Issue, IssueSeverity +from codespy.workflows.review.models import ReviewResult +from codespy.workflows.review.reporters.base import BaseReporter from codespy.tools.git.client import get_client if TYPE_CHECKING: diff --git a/src/codespy/agents/reviewer/reporters/stdout.py b/src/codespy/workflows/review/reporters/stdout.py similarity index 88% rename from src/codespy/agents/reviewer/reporters/stdout.py rename to src/codespy/workflows/review/reporters/stdout.py index e766512..2d14b2f 100644 --- a/src/codespy/agents/reviewer/reporters/stdout.py +++ b/src/codespy/workflows/review/reporters/stdout.py @@ -5,8 +5,8 @@ from rich.console import Console -from codespy.agents.reviewer.models import ReviewResult -from codespy.agents.reviewer.reporters.base import BaseReporter +from codespy.workflows.review.models import ReviewResult +from codespy.workflows.review.reporters.base import BaseReporter class StdoutReporter(BaseReporter): diff --git a/src/codespy/agents/reviewer/server.py b/src/codespy/workflows/review/server.py similarity index 96% rename from src/codespy/agents/reviewer/server.py rename to src/codespy/workflows/review/server.py index b38f1d1..035ff0c 100644 --- a/src/codespy/agents/reviewer/server.py +++ b/src/codespy/workflows/review/server.py @@ -26,7 +26,7 @@ def _get_pipeline() -> Any: """Get or create the ReviewPipeline (lazy init to avoid slow startup).""" global _pipeline if _pipeline is None: - from codespy.agents.reviewer.reviewer import ReviewPipeline + from codespy.workflows.review.pipeline import ReviewPipeline _pipeline = ReviewPipeline(_settings) return _pipeline @@ -47,7 +47,7 @@ def _do_local_review(repo_path: str, base_ref: str, output_format: str) -> str: """Synchronous local review — runs in thread pool.""" import json - from codespy.agents.reviewer.models import LocalReviewConfig + from codespy.workflows.review.models import LocalReviewConfig repo = Path(repo_path).resolve() config = LocalReviewConfig(repo_path=repo, base_ref=base_ref, uncommitted=False) @@ -64,7 +64,7 @@ def _do_uncommitted_review(repo_path: str, output_format: str) -> str: """Synchronous uncommitted review — runs in thread pool.""" import json - from codespy.agents.reviewer.models import LocalReviewConfig + from codespy.workflows.review.models import LocalReviewConfig repo = Path(repo_path).resolve() config = LocalReviewConfig(repo_path=repo, uncommitted=True) @@ -81,7 +81,7 @@ def _do_pr_review(pr_url: str, output_format: str) -> str: """Synchronous PR review — runs in thread pool.""" import json - from codespy.agents.reviewer.models import RemoteReviewConfig + from codespy.workflows.review.models import RemoteReviewConfig config = RemoteReviewConfig(url=pr_url) diff --git a/tests/test_context_memory.py b/tests/test_context_memory.py index 4dcf85d..ead7b6f 100644 --- a/tests/test_context_memory.py +++ b/tests/test_context_memory.py @@ -14,7 +14,7 @@ compute_common_ancestor_topic_id, make_topic_id, ) -from codespy.agents.reviewer.modules.manifest_parser import extract_package_name +from codespy.agents.review.scope.manifest_parser import extract_package_name class TestOperationAliases: @@ -338,7 +338,7 @@ class TestScopeResultTopicHelper: def test_topic_with_package_manifest(self): """Topic uses package_name from manifest when available.""" - from codespy.agents.reviewer.models import PackageManifest, ScopeResult, ScopeType + from codespy.agents.review.scope.models import PackageManifest, ScopeResult, ScopeType scope = ScopeResult( repo="owner/repo", @@ -359,7 +359,7 @@ def test_topic_with_package_manifest(self): def test_topic_without_package_manifest(self): """Topic uses subroot when no package manifest.""" - from codespy.agents.reviewer.models import ScopeResult, ScopeType + from codespy.agents.review.scope.models import ScopeResult, ScopeType scope = ScopeResult( repo="owner/repo", @@ -375,7 +375,7 @@ def test_topic_without_package_manifest(self): def test_topic_for_root_scope(self): """Root scope topic uses just repo_full_name.""" - from codespy.agents.reviewer.models import ScopeResult, ScopeType + from codespy.agents.review.scope.models import ScopeResult, ScopeType scope = ScopeResult( repo="owner/repo", diff --git a/tests/test_patch_utils.py b/tests/test_patch_utils.py index fdb6f1d..dce0993 100644 --- a/tests/test_patch_utils.py +++ b/tests/test_patch_utils.py @@ -3,7 +3,7 @@ from pathlib import Path from unittest.mock import MagicMock -from codespy.agents.reviewer.models import ScopeResult, ScopeType +from codespy.agents.review.scope.models import ScopeResult, ScopeType from codespy.tools.git.models import ChangedFile, FileStatus from codespy.tools.git.patch_utils import ( _expand_hunk_to_functions, diff --git a/tests/test_scope_resolver.py b/tests/test_scope_resolver.py index 58e62d0..8c029a6 100644 --- a/tests/test_scope_resolver.py +++ b/tests/test_scope_resolver.py @@ -5,8 +5,8 @@ import pytest -from codespy.agents.reviewer.models import ScopeType -from codespy.agents.reviewer.modules.scope_resolver import ( +from codespy.agents.review.scope.models import ScopeType +from codespy.agents.review.scope import ( ScopeResolver, derive_sparse_paths, ) @@ -291,7 +291,7 @@ class TestAssignFilesDeterminism: def test_same_depth_scopes_deterministic(self): """Scopes at the same depth should assign files consistently.""" - from codespy.agents.reviewer.models import ScopeResult, ScopeType + from codespy.agents.review.scope.models import ScopeResult, ScopeType # Two scopes at depth 1 (one slash each) scope_a = ScopeResult(subroot="packages/alpha", scope_type=ScopeType.LIBRARY, reason="test")