codespy-ai 0.3.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/PKG-INFO +11 -8
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/README.md +10 -7
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/pyproject.toml +1 -1
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/__init__.py +1 -1
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/cost_tracker.py +2 -2
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/models.py +8 -1
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/modules/__init__.py +4 -2
- codespy_ai-0.3.2/src/codespy/agents/reviewer/modules/code_reviewer.py +266 -0
- codespy_ai-0.3.2/src/codespy/agents/reviewer/modules/doc_extractor.py +80 -0
- codespy_ai-0.3.2/src/codespy/agents/reviewer/modules/doc_reviewer.py +197 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/modules/helpers.py +0 -8
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/modules/scope_identifier.py +47 -27
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/reviewer.py +9 -5
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/config_dspy.py +5 -4
- codespy_ai-0.3.0/src/codespy/agents/reviewer/modules/code_and_doc_reviewer.py +0 -291
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/LICENSE +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/__init__.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/dspy_config.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/__init__.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/modules/deduplicator.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/reporters/__init__.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/reporters/base.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/reporters/git.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/reporters/stdout.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/server.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/cli.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/cli_local.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/cli_mcp_server.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/cli_remote.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/config.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/config_git.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/config_io.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/config_llm.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/__init__.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/cyber/__init__.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/cyber/osv/__init__.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/cyber/osv/client.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/cyber/osv/models.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/cyber/osv/server.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/filesystem/__init__.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/filesystem/client.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/filesystem/models.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/filesystem/server.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/git/__init__.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/git/base.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/git/client.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/git/github_client.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/git/gitlab_client.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/git/local_diff.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/git/models.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/git/server.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/mcp_utils.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/__init__.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/ripgrep/__init__.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/ripgrep/client.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/ripgrep/server.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/__init__.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/base_extractor.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/extractors/__init__.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/extractors/go.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/extractors/java.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/extractors/javascript.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/extractors/kotlin.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/extractors/objc.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/extractors/python.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/extractors/rust.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/extractors/swift.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/extractors/terraform.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/models.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/parser.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/server.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/web/__init__.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/web/client.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/web/models.py +0 -0
- {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/web/server.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codespy-ai
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Code review agent powered by DSPy
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -537,7 +537,7 @@ export DEFAULT_MODEL=anthropic/claude-opus-4-6
|
|
|
537
537
|
export DEFAULT_MAX_ITERS=20
|
|
538
538
|
|
|
539
539
|
# Per-signature settings (use signature name, not module name)
|
|
540
|
-
export
|
|
540
|
+
export CODE_REVIEW_MODEL=anthropic/claude-sonnet-4-5-20250929
|
|
541
541
|
|
|
542
542
|
# Output settings
|
|
543
543
|
export OUTPUT_STDOUT=false
|
|
@@ -693,10 +693,12 @@ output_git: true
|
|
|
693
693
|
│ │ │
|
|
694
694
|
│ ┌──────────────────────────▼─────────────────────────────────┐ │
|
|
695
695
|
│ │ Parallel Review Modules │ │
|
|
696
|
-
│ │ ┌──────────────┐
|
|
697
|
-
│ │ │Supply Chain
|
|
698
|
-
│ │ │
|
|
699
|
-
│ │
|
|
696
|
+
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │ │
|
|
697
|
+
│ │ │ Supply Chain │ │ Code │ │ Doc │ │ │
|
|
698
|
+
│ │ │ Auditor │ │ Reviewer │ │ Reviewer │ │ │
|
|
699
|
+
│ │ │ │ │ (bug+sec+ │ │ │ │ │
|
|
700
|
+
│ │ │ │ │ smell) │ │ │ │ │
|
|
701
|
+
│ │ └──────────────┘ └──────────────┘ └──────────┘ │ │
|
|
700
702
|
│ └──────────────────────────┬─────────────────────────────────┘ │
|
|
701
703
|
│ │ │
|
|
702
704
|
│ ┌──────────────────────────▼─────────────────────────────────┐ │
|
|
@@ -739,8 +741,9 @@ The review is powered by DSPy signatures that structure the LLM's analysis:
|
|
|
739
741
|
|
|
740
742
|
| Signature | Config Key | Description |
|
|
741
743
|
|-----------|------------|-------------|
|
|
742
|
-
| **ScopeIdentifierSignature** | `
|
|
743
|
-
| **
|
|
744
|
+
| **ScopeIdentifierSignature** | `scope` | Identifies code scopes (frontend, backend, infra, microservice in mono repo, etc...) |
|
|
745
|
+
| **CodeReviewSignature** | `code_review` | Detects verified bugs, security vulnerabilities, removed defensive code, and code smells |
|
|
746
|
+
| **DocReviewSignature** | `doc` | Detects stale or wrong documentation caused by code changes |
|
|
744
747
|
| **SupplyChainSecuritySignature** | `supply_chain` | Analyzes artifacts (Dockerfiles) and dependencies for supply chain security |
|
|
745
748
|
| **IssueDeduplicationSignature** | `deduplication` | LLM-powered deduplication of issues across reviewers |
|
|
746
749
|
| **MRSummarySignature** | `summarization` | Generates summary, quality assessment, and recommendation |
|
|
@@ -486,7 +486,7 @@ export DEFAULT_MODEL=anthropic/claude-opus-4-6
|
|
|
486
486
|
export DEFAULT_MAX_ITERS=20
|
|
487
487
|
|
|
488
488
|
# Per-signature settings (use signature name, not module name)
|
|
489
|
-
export
|
|
489
|
+
export CODE_REVIEW_MODEL=anthropic/claude-sonnet-4-5-20250929
|
|
490
490
|
|
|
491
491
|
# Output settings
|
|
492
492
|
export OUTPUT_STDOUT=false
|
|
@@ -642,10 +642,12 @@ output_git: true
|
|
|
642
642
|
│ │ │
|
|
643
643
|
│ ┌──────────────────────────▼─────────────────────────────────┐ │
|
|
644
644
|
│ │ Parallel Review Modules │ │
|
|
645
|
-
│ │ ┌──────────────┐
|
|
646
|
-
│ │ │Supply Chain
|
|
647
|
-
│ │ │
|
|
648
|
-
│ │
|
|
645
|
+
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │ │
|
|
646
|
+
│ │ │ Supply Chain │ │ Code │ │ Doc │ │ │
|
|
647
|
+
│ │ │ Auditor │ │ Reviewer │ │ Reviewer │ │ │
|
|
648
|
+
│ │ │ │ │ (bug+sec+ │ │ │ │ │
|
|
649
|
+
│ │ │ │ │ smell) │ │ │ │ │
|
|
650
|
+
│ │ └──────────────┘ └──────────────┘ └──────────┘ │ │
|
|
649
651
|
│ └──────────────────────────┬─────────────────────────────────┘ │
|
|
650
652
|
│ │ │
|
|
651
653
|
│ ┌──────────────────────────▼─────────────────────────────────┐ │
|
|
@@ -688,8 +690,9 @@ The review is powered by DSPy signatures that structure the LLM's analysis:
|
|
|
688
690
|
|
|
689
691
|
| Signature | Config Key | Description |
|
|
690
692
|
|-----------|------------|-------------|
|
|
691
|
-
| **ScopeIdentifierSignature** | `
|
|
692
|
-
| **
|
|
693
|
+
| **ScopeIdentifierSignature** | `scope` | Identifies code scopes (frontend, backend, infra, microservice in mono repo, etc...) |
|
|
694
|
+
| **CodeReviewSignature** | `code_review` | Detects verified bugs, security vulnerabilities, removed defensive code, and code smells |
|
|
695
|
+
| **DocReviewSignature** | `doc` | Detects stale or wrong documentation caused by code changes |
|
|
693
696
|
| **SupplyChainSecuritySignature** | `supply_chain` | Analyzes artifacts (Dockerfiles) and dependencies for supply chain security |
|
|
694
697
|
| **IssueDeduplicationSignature** | `deduplication` | LLM-powered deduplication of issues across reviewers |
|
|
695
698
|
| **MRSummarySignature** | `summarization` | Generates summary, quality assessment, and recommendation |
|
|
@@ -206,8 +206,8 @@ class SignatureContext:
|
|
|
206
206
|
3. Summing costs/tokens from new entries
|
|
207
207
|
|
|
208
208
|
Usage:
|
|
209
|
-
with SignatureContext("
|
|
210
|
-
# All LLM calls here will be attributed to
|
|
209
|
+
with SignatureContext("code_review", cost_tracker):
|
|
210
|
+
# All LLM calls here will be attributed to code_review
|
|
211
211
|
result = await agent.acall(...)
|
|
212
212
|
"""
|
|
213
213
|
|
|
@@ -23,6 +23,7 @@ class IssueCategory(str, Enum):
|
|
|
23
23
|
SECURITY = "security"
|
|
24
24
|
BUG = "bug"
|
|
25
25
|
DOCUMENTATION = "documentation"
|
|
26
|
+
SMELL = "smell"
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
class ScopeType(str, Enum):
|
|
@@ -107,7 +108,7 @@ class Issue(BaseModel):
|
|
|
107
108
|
class SignatureStatsResult(BaseModel):
|
|
108
109
|
"""Statistics for a single signature's execution during review."""
|
|
109
110
|
|
|
110
|
-
name: str = Field(description="Signature name (e.g.,
|
|
111
|
+
name: str = Field(description="Signature name (e.g., code_review, doc, scope, supply_chain, deduplication, summarization)")
|
|
111
112
|
cost: float = Field(default=0.0, description="Cost in USD for this signature")
|
|
112
113
|
tokens: int = Field(default=0, description="Tokens used by this signature")
|
|
113
114
|
call_count: int = Field(default=0, description="Number of LLM calls made by this signature")
|
|
@@ -183,6 +184,11 @@ class ReviewResult(BaseModel):
|
|
|
183
184
|
"""Get all documentation issues."""
|
|
184
185
|
return [i for i in self.issues if i.category == IssueCategory.DOCUMENTATION]
|
|
185
186
|
|
|
187
|
+
@property
|
|
188
|
+
def smell_issues(self) -> list[Issue]:
|
|
189
|
+
"""Get all code smell issues."""
|
|
190
|
+
return [i for i in self.issues if i.category == IssueCategory.SMELL]
|
|
191
|
+
|
|
186
192
|
def issues_by_severity(self) -> dict[IssueSeverity, list[Issue]]:
|
|
187
193
|
"""Group issues by severity."""
|
|
188
194
|
result: dict[IssueSeverity, list[Issue]] = {s: [] for s in IssueSeverity}
|
|
@@ -218,6 +224,7 @@ class ReviewResult(BaseModel):
|
|
|
218
224
|
f"- **Security:** {len(self.security_issues)}",
|
|
219
225
|
f"- **Bugs:** {len(self.bug_issues)}",
|
|
220
226
|
f"- **Documentation:** {len(self.documentation_issues)}",
|
|
227
|
+
f"- **Smells:** {len(self.smell_issues)}",
|
|
221
228
|
"",
|
|
222
229
|
])
|
|
223
230
|
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"""DSPy modules for code review."""
|
|
2
2
|
|
|
3
|
-
from codespy.agents.reviewer.modules.
|
|
3
|
+
from codespy.agents.reviewer.modules.code_reviewer import CodeReviewer
|
|
4
4
|
from codespy.agents.reviewer.modules.deduplicator import IssueDeduplicator
|
|
5
|
+
from codespy.agents.reviewer.modules.doc_reviewer import DocReviewer
|
|
5
6
|
from codespy.agents.reviewer.modules.scope_identifier import ScopeIdentifier
|
|
6
7
|
from codespy.agents.reviewer.modules.supply_chain_auditor import SupplyChainAuditor
|
|
7
8
|
|
|
8
9
|
__all__ = [
|
|
9
|
-
"
|
|
10
|
+
"CodeReviewer",
|
|
11
|
+
"DocReviewer",
|
|
10
12
|
"IssueDeduplicator",
|
|
11
13
|
"ScopeIdentifier",
|
|
12
14
|
"SupplyChainAuditor",
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Unified code review module — defects, security, and code smells."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Sequence
|
|
7
|
+
|
|
8
|
+
import dspy # type: ignore[import-untyped]
|
|
9
|
+
|
|
10
|
+
from codespy.agents import SignatureContext, get_cost_tracker
|
|
11
|
+
from codespy.agents.reviewer.models import Issue, IssueCategory, ScopeResult
|
|
12
|
+
from codespy.agents.reviewer.modules.helpers import (
|
|
13
|
+
MIN_CONFIDENCE,
|
|
14
|
+
make_scope_relative,
|
|
15
|
+
resolve_scope_root,
|
|
16
|
+
restore_repo_paths,
|
|
17
|
+
)
|
|
18
|
+
from codespy.config import get_settings
|
|
19
|
+
from codespy.tools.mcp_utils import cleanup_mcp_contexts, connect_mcp_server
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CodeReviewSignature(dspy.Signature):
|
|
25
|
+
"""Detect VERIFIED code defects, security vulnerabilities, and code smells.
|
|
26
|
+
|
|
27
|
+
You are a busy Principal Engineer with very little time.
|
|
28
|
+
Be extremely terse. Use imperative mood ("Fix X", not "You should fix X").
|
|
29
|
+
You have tools to explore the scope's filesystem, search for text, and analyze code.
|
|
30
|
+
All file paths are relative to the scope root directory (the current tool root).
|
|
31
|
+
Tools are restricted to this scope — you cannot access files outside it.
|
|
32
|
+
|
|
33
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
34
|
+
ANALYZE CHANGES
|
|
35
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
36
|
+
|
|
37
|
+
Review each changed file's patch. For each file, check ALL categories (A, B, C)
|
|
38
|
+
before moving to the next file.
|
|
39
|
+
|
|
40
|
+
TOOLS AVAILABLE:
|
|
41
|
+
- find_function_definitions: check function signatures and implementations
|
|
42
|
+
- find_function_calls: understand how functions are called, trace data flow
|
|
43
|
+
- find_function_usages / find_callers: trace usage patterns
|
|
44
|
+
- search_literal: find related patterns (sanitization, validation, naming)
|
|
45
|
+
- read_file:broader context not visible in the patch (use sparingly)
|
|
46
|
+
|
|
47
|
+
─── A. BUGS & LOGIC ERRORS (category = "bug") ───────────────────────────────
|
|
48
|
+
|
|
49
|
+
EVIDENCE STANDARD: STRICT — you must VERIFY with tools before reporting.
|
|
50
|
+
DO NOT speculate. No "might be", "could be", "possibly" issues.
|
|
51
|
+
If you cannot point to the EXACT defective code with evidence, do NOT report.
|
|
52
|
+
|
|
53
|
+
- Logic errors: verify the condition is actually incorrect by checking related code
|
|
54
|
+
- Null/undefined references: verify the check is actually missing
|
|
55
|
+
- Resource leaks: verify there's no cleanup in finally/defer/close methods
|
|
56
|
+
- Error handling: verify errors aren't handled elsewhere in the call chain
|
|
57
|
+
- Type mismatches: verify types by checking definitions
|
|
58
|
+
- Off-by-one errors: verify by understanding the data structure bounds
|
|
59
|
+
|
|
60
|
+
REMOVED DEFENSIVE CODE (always report as "bug"):
|
|
61
|
+
If a guard (bounds check, null check, Math.max/min, default value, try/catch,
|
|
62
|
+
sanitization) existed in removed lines (-) and is absent from added lines (+),
|
|
63
|
+
report it. Use read_file or search_literal to confirm the guard was not relocated
|
|
64
|
+
elsewhere in the same function/file. If it was moved, do not report.
|
|
65
|
+
|
|
66
|
+
─── B. SECURITY VULNERABILITIES (category = "security") ─────────────────────
|
|
67
|
+
|
|
68
|
+
EVIDENCE STANDARD: STRICT — you must VERIFY with tools before reporting.
|
|
69
|
+
DO NOT speculate. Trace data flow from source to sink.
|
|
70
|
+
|
|
71
|
+
- Injection (SQL, command, XSS): verify input reaches dangerous sink unsanitized
|
|
72
|
+
- Authentication/authorization: verify auth check is actually missing
|
|
73
|
+
- Sensitive data exposure: verify data is actually exposed, not just accessed
|
|
74
|
+
- Insecure crypto: verify the actual algorithm used
|
|
75
|
+
- Path traversal: verify path input is not sanitized
|
|
76
|
+
- Race conditions: verify shared state access without synchronization
|
|
77
|
+
|
|
78
|
+
─── C. CODE SMELLS (category = "smell") ──────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
EVIDENCE STANDARD: OBSERVATIONAL — the patch or function signature IS the evidence.
|
|
81
|
+
A function with 5 primitive parameters IS a smell — no further verification needed.
|
|
82
|
+
A variable named "data" IS a smell — the name itself is the evidence.
|
|
83
|
+
Use tools to confirm if needed (e.g., find_function_definitions for param lists),
|
|
84
|
+
but the structural pattern visible in the patch is sufficient to report.
|
|
85
|
+
|
|
86
|
+
UNCOMMUNICATIVE NAMES:
|
|
87
|
+
- Variables not describing data they hold (data, info, item, temp, val)
|
|
88
|
+
- Functions not starting with a verb (user(), process())
|
|
89
|
+
- Booleans not reading as predicates (valid, flag → isValid, hasPermission)
|
|
90
|
+
- Side-effect mismatch: getName() that writes to DB
|
|
91
|
+
|
|
92
|
+
PRIMITIVE OBSESSION:
|
|
93
|
+
- Functions taking 3+ related primitive parameters that travel together
|
|
94
|
+
Example: (string zip, string city, string street) → suggest Address struct/class
|
|
95
|
+
- Use find_function_definitions to inspect parameter lists if not visible in patch
|
|
96
|
+
|
|
97
|
+
COMPLEXITY:
|
|
98
|
+
- Double negatives, nested ternaries, >2 logical operators per condition
|
|
99
|
+
- Magic numbers in logic → suggest named constants
|
|
100
|
+
- 3+ nesting levels → suggest guard clauses/early returns
|
|
101
|
+
- 5+ switch/if-elif branches → suggest polymorphism
|
|
102
|
+
|
|
103
|
+
YAGNI:
|
|
104
|
+
- Abstract class/interface with single implementation (verify with find_callers)
|
|
105
|
+
- Unused parameters accepted "for future use"
|
|
106
|
+
|
|
107
|
+
DO NOT report as smells:
|
|
108
|
+
- Idiomatic short names (i, j, k, err, ctx, db, tx)
|
|
109
|
+
- Test file naming conventions
|
|
110
|
+
|
|
111
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
112
|
+
OUTPUT RULES
|
|
113
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
114
|
+
|
|
115
|
+
- Set category to one of the values provided in the categories input
|
|
116
|
+
- For security issues, include cwe_id where applicable
|
|
117
|
+
- Reference files by name and line number only — never copy source code into issues
|
|
118
|
+
- Do not repeat patch content in reasoning steps. Keep each step to 1-2 sentences
|
|
119
|
+
- Empty list if no issues found. No approval text ("LGTM", "looks good")
|
|
120
|
+
- description: ≤25 words, imperative tone, no filler ("Fix X", "Rename Y to Z")
|
|
121
|
+
- No polite or conversational language
|
|
122
|
+
- Do not populate code_snippet — use line numbers instead
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
scope: ScopeResult = dspy.InputField(
|
|
126
|
+
desc="Scope with changed files. Has: subroot, scope_type, "
|
|
127
|
+
"changed_files (filename + patch - analyze patch first), language, package_manifest. "
|
|
128
|
+
"File paths in changed_files are relative to the scope root (tool root)."
|
|
129
|
+
)
|
|
130
|
+
categories: list[IssueCategory] = dspy.InputField(
|
|
131
|
+
desc="Allowed issue categories. Use only these values for the 'category' field on each issue."
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
issues: list[Issue] = dspy.OutputField(
|
|
135
|
+
desc="Verified issues. Category must be one of the provided categories. "
|
|
136
|
+
"Titles <10 words. Descriptions ≤25 words, imperative. Empty list if none. "
|
|
137
|
+
"File paths must be relative to scope root."
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class CodeReviewer(dspy.Module):
|
|
142
|
+
"""Unified code reviewer — defects, security, and smells in a single pass.
|
|
143
|
+
|
|
144
|
+
Merges DefectDetector and SmellDetector into one agent to avoid redundant
|
|
145
|
+
README reads, tool sessions, and input token costs per scope.
|
|
146
|
+
|
|
147
|
+
MCP tools are scope-restricted: for each scope, tools are rooted at
|
|
148
|
+
repo_path/scope.subroot so the agent cannot access files outside the scope.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
def __init__(self) -> None:
|
|
152
|
+
"""Initialize the code reviewer."""
|
|
153
|
+
super().__init__()
|
|
154
|
+
self._cost_tracker = get_cost_tracker()
|
|
155
|
+
self._settings = get_settings()
|
|
156
|
+
|
|
157
|
+
async def _create_tools(
|
|
158
|
+
self, scope_root: Path
|
|
159
|
+
) -> tuple[list[Any], list[Any]]:
|
|
160
|
+
"""Create scope-restricted tools: filesystem + ripgrep + treesitter."""
|
|
161
|
+
tools: list[Any] = []
|
|
162
|
+
contexts: list[Any] = []
|
|
163
|
+
tools_dir = Path(__file__).parent.parent.parent.parent / "tools"
|
|
164
|
+
scope_root_str = str(scope_root)
|
|
165
|
+
caller = "code_reviewer"
|
|
166
|
+
|
|
167
|
+
tools.extend(await connect_mcp_server(
|
|
168
|
+
tools_dir / "filesystem" / "server.py",
|
|
169
|
+
[scope_root_str], contexts, caller,
|
|
170
|
+
))
|
|
171
|
+
tools.extend(await connect_mcp_server(
|
|
172
|
+
tools_dir / "parsers" / "ripgrep" / "server.py",
|
|
173
|
+
[scope_root_str], contexts, caller,
|
|
174
|
+
))
|
|
175
|
+
tools.extend(await connect_mcp_server(
|
|
176
|
+
tools_dir / "parsers" / "treesitter" / "server.py",
|
|
177
|
+
[scope_root_str], contexts, caller,
|
|
178
|
+
))
|
|
179
|
+
return tools, contexts
|
|
180
|
+
|
|
181
|
+
async def aforward(
|
|
182
|
+
self, scopes: Sequence[ScopeResult], repo_path: Path
|
|
183
|
+
) -> list[Issue]:
|
|
184
|
+
"""Analyze scopes for defects, security issues, and code smells.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
scopes: List of identified scopes with their changed files
|
|
188
|
+
repo_path: Path to the cloned repository
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
List of bug, security, and smell issues found across all scopes
|
|
192
|
+
"""
|
|
193
|
+
if not self._settings.is_signature_enabled("code_review"):
|
|
194
|
+
logger.debug("Skipping code_review: disabled")
|
|
195
|
+
return []
|
|
196
|
+
|
|
197
|
+
# Determine which categories are active
|
|
198
|
+
categories: list[IssueCategory] = []
|
|
199
|
+
categories.append(IssueCategory.BUG)
|
|
200
|
+
categories.append(IssueCategory.SECURITY)
|
|
201
|
+
categories.append(IssueCategory.SMELL)
|
|
202
|
+
|
|
203
|
+
changed_scopes = [s for s in scopes if s.has_changes and s.changed_files]
|
|
204
|
+
if not changed_scopes:
|
|
205
|
+
logger.info("No scopes with changes for code review")
|
|
206
|
+
return []
|
|
207
|
+
|
|
208
|
+
all_issues: list[Issue] = []
|
|
209
|
+
max_iters = self._settings.get_max_iters("code_review")
|
|
210
|
+
|
|
211
|
+
total_files = sum(len(s.changed_files) for s in changed_scopes)
|
|
212
|
+
logger.info(
|
|
213
|
+
f"Code review for {len(changed_scopes)} scopes "
|
|
214
|
+
f"({total_files} changed files)..."
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
for scope in changed_scopes:
|
|
218
|
+
scope_root = resolve_scope_root(repo_path, scope.subroot)
|
|
219
|
+
tools, contexts = await self._create_tools(scope_root)
|
|
220
|
+
try:
|
|
221
|
+
agent = dspy.ReAct(
|
|
222
|
+
signature=CodeReviewSignature,
|
|
223
|
+
tools=tools,
|
|
224
|
+
max_iters=max_iters,
|
|
225
|
+
)
|
|
226
|
+
scoped = make_scope_relative(scope)
|
|
227
|
+
logger.info(
|
|
228
|
+
f" Code review: scope {scope.subroot} "
|
|
229
|
+
f"({len(scope.changed_files)} files)"
|
|
230
|
+
)
|
|
231
|
+
async with SignatureContext("code_review", self._cost_tracker):
|
|
232
|
+
result = await agent.acall(
|
|
233
|
+
scope=scoped,
|
|
234
|
+
categories=categories,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
issues = [
|
|
238
|
+
issue for issue in (result.issues or [])
|
|
239
|
+
if issue.confidence >= MIN_CONFIDENCE
|
|
240
|
+
]
|
|
241
|
+
restore_repo_paths(issues, scope.subroot)
|
|
242
|
+
all_issues.extend(issues)
|
|
243
|
+
logger.debug(
|
|
244
|
+
f" Scope {scope.subroot}: {len(issues)} code review issues"
|
|
245
|
+
)
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.error(f"Code review failed for scope {scope.subroot}: {e}")
|
|
248
|
+
finally:
|
|
249
|
+
await cleanup_mcp_contexts(contexts)
|
|
250
|
+
|
|
251
|
+
logger.info(f"Code review found {len(all_issues)} issues")
|
|
252
|
+
return all_issues
|
|
253
|
+
|
|
254
|
+
def forward(
|
|
255
|
+
self, scopes: Sequence[ScopeResult], repo_path: Path
|
|
256
|
+
) -> list[Issue]:
|
|
257
|
+
"""Analyze scopes for code issues (sync wrapper).
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
scopes: List of identified scopes with their changed files
|
|
261
|
+
repo_path: Path to the cloned repository
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
List of bug, security, and smell issues found across all scopes
|
|
265
|
+
"""
|
|
266
|
+
return asyncio.run(self.aforward(scopes, repo_path))
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Deterministic documentation extractor — single tree scan, no LLM."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from codespy.tools.filesystem.client import FileSystem
|
|
8
|
+
from codespy.tools.filesystem.models import EntryType, TreeNode
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
# Filename patterns recognised as documentation (case-insensitive).
|
|
13
|
+
_DOC_FILE_RE = re.compile(
|
|
14
|
+
r"^(readme|changelog|contributing|\.env\.example)",
|
|
15
|
+
re.IGNORECASE,
|
|
16
|
+
)
|
|
17
|
+
# Top-level directory names whose *entire* contents count as docs.
|
|
18
|
+
_DOC_DIRS = {"docs", "documentation"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _collect_all_files(node: TreeNode, prefix: str) -> list[str]:
|
|
22
|
+
"""Recursively collect all file paths under a tree node."""
|
|
23
|
+
paths: list[str] = []
|
|
24
|
+
for child in node.children:
|
|
25
|
+
rel = f"{prefix}{child.name}"
|
|
26
|
+
if child.entry_type == EntryType.DIRECTORY:
|
|
27
|
+
paths.extend(_collect_all_files(child, f"{rel}/"))
|
|
28
|
+
else:
|
|
29
|
+
paths.append(rel)
|
|
30
|
+
return paths
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _collect_doc_paths(node: TreeNode, prefix: str = "") -> list[str]:
|
|
34
|
+
"""Walk a shallow tree and return relative paths of doc files.
|
|
35
|
+
|
|
36
|
+
Matches:
|
|
37
|
+
- Root-level files: README*, CHANGELOG*, CONTRIBUTING*, .env.example
|
|
38
|
+
- Everything under docs/ or documentation/ directories
|
|
39
|
+
"""
|
|
40
|
+
paths: list[str] = []
|
|
41
|
+
for child in node.children:
|
|
42
|
+
rel = f"{prefix}{child.name}" if prefix else child.name
|
|
43
|
+
if child.entry_type == EntryType.DIRECTORY:
|
|
44
|
+
if child.name.lower() in _DOC_DIRS:
|
|
45
|
+
paths.extend(_collect_all_files(child, f"{rel}/"))
|
|
46
|
+
else:
|
|
47
|
+
# Recurse into non-doc subdirs (depth-2 tree already limits this).
|
|
48
|
+
paths.extend(_collect_doc_paths(child, f"{rel}/"))
|
|
49
|
+
elif _DOC_FILE_RE.match(child.name):
|
|
50
|
+
paths.append(rel)
|
|
51
|
+
return paths
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def extract_documentation(scope_root: Path) -> str:
|
|
55
|
+
"""Extract documentation content from a scope directory.
|
|
56
|
+
|
|
57
|
+
Single tree scan at depth 2, fast-fail if no doc files found.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Concatenated documentation with ``=== filename ===`` headers,
|
|
61
|
+
or empty string if no documentation exists.
|
|
62
|
+
"""
|
|
63
|
+
fs = FileSystem(scope_root, create_if_missing=False)
|
|
64
|
+
|
|
65
|
+
# One tree scan — depth 2 covers root files + immediate subdirs.
|
|
66
|
+
tree = fs.get_tree(max_depth=2)
|
|
67
|
+
doc_paths = _collect_doc_paths(tree)
|
|
68
|
+
|
|
69
|
+
if not doc_paths:
|
|
70
|
+
return ""
|
|
71
|
+
|
|
72
|
+
parts: list[str] = []
|
|
73
|
+
for path in doc_paths:
|
|
74
|
+
try:
|
|
75
|
+
content = fs.read_file(path)
|
|
76
|
+
parts.append(f"=== {path} ===\n{content.content}")
|
|
77
|
+
except Exception as e: # noqa: BLE001
|
|
78
|
+
logger.warning(f"Could not read doc file {path}: {e}")
|
|
79
|
+
|
|
80
|
+
return "\n\n".join(parts)
|