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.
Files changed (76) hide show
  1. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/PKG-INFO +11 -8
  2. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/README.md +10 -7
  3. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/pyproject.toml +1 -1
  4. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/__init__.py +1 -1
  5. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/cost_tracker.py +2 -2
  6. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/models.py +8 -1
  7. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/modules/__init__.py +4 -2
  8. codespy_ai-0.3.2/src/codespy/agents/reviewer/modules/code_reviewer.py +266 -0
  9. codespy_ai-0.3.2/src/codespy/agents/reviewer/modules/doc_extractor.py +80 -0
  10. codespy_ai-0.3.2/src/codespy/agents/reviewer/modules/doc_reviewer.py +197 -0
  11. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/modules/helpers.py +0 -8
  12. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/modules/scope_identifier.py +47 -27
  13. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/reviewer.py +9 -5
  14. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/config_dspy.py +5 -4
  15. codespy_ai-0.3.0/src/codespy/agents/reviewer/modules/code_and_doc_reviewer.py +0 -291
  16. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/LICENSE +0 -0
  17. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/__init__.py +0 -0
  18. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/dspy_config.py +0 -0
  19. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/__init__.py +0 -0
  20. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/modules/deduplicator.py +0 -0
  21. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +0 -0
  22. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/reporters/__init__.py +0 -0
  23. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/reporters/base.py +0 -0
  24. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/reporters/git.py +0 -0
  25. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/reporters/stdout.py +0 -0
  26. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/agents/reviewer/server.py +0 -0
  27. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/cli.py +0 -0
  28. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/cli_local.py +0 -0
  29. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/cli_mcp_server.py +0 -0
  30. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/cli_remote.py +0 -0
  31. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/config.py +0 -0
  32. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/config_git.py +0 -0
  33. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/config_io.py +0 -0
  34. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/config_llm.py +0 -0
  35. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/__init__.py +0 -0
  36. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/cyber/__init__.py +0 -0
  37. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/cyber/osv/__init__.py +0 -0
  38. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/cyber/osv/client.py +0 -0
  39. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/cyber/osv/models.py +0 -0
  40. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/cyber/osv/server.py +0 -0
  41. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/filesystem/__init__.py +0 -0
  42. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/filesystem/client.py +0 -0
  43. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/filesystem/models.py +0 -0
  44. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/filesystem/server.py +0 -0
  45. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/git/__init__.py +0 -0
  46. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/git/base.py +0 -0
  47. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/git/client.py +0 -0
  48. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/git/github_client.py +0 -0
  49. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/git/gitlab_client.py +0 -0
  50. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/git/local_diff.py +0 -0
  51. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/git/models.py +0 -0
  52. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/git/server.py +0 -0
  53. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/mcp_utils.py +0 -0
  54. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/__init__.py +0 -0
  55. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/ripgrep/__init__.py +0 -0
  56. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/ripgrep/client.py +0 -0
  57. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/ripgrep/server.py +0 -0
  58. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/__init__.py +0 -0
  59. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/base_extractor.py +0 -0
  60. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/extractors/__init__.py +0 -0
  61. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/extractors/go.py +0 -0
  62. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/extractors/java.py +0 -0
  63. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/extractors/javascript.py +0 -0
  64. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/extractors/kotlin.py +0 -0
  65. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/extractors/objc.py +0 -0
  66. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/extractors/python.py +0 -0
  67. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/extractors/rust.py +0 -0
  68. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/extractors/swift.py +0 -0
  69. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/extractors/terraform.py +0 -0
  70. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/models.py +0 -0
  71. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/parser.py +0 -0
  72. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/parsers/treesitter/server.py +0 -0
  73. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/web/__init__.py +0 -0
  74. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/web/client.py +0 -0
  75. {codespy_ai-0.3.0 → codespy_ai-0.3.2}/src/codespy/tools/web/models.py +0 -0
  76. {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.0
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 CODE_AND_DOC_REVIEW_MODEL=anthropic/claude-sonnet-4-5-20250929
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 │ │ Code & Doc Reviewer │ │ │
698
- │ │ │ Auditor │ │ (defects + documentation) │ │ │
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** | `scope_identification` | Identifies code scopes (frontend, backend, infra, microservice in mono repo, etc...) |
743
- | **CodeAndDocReviewSignature** | `code_and_doc_review` | Detects verified bugs, security vulnerabilities, and stale/wrong documentation in a single pass |
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 CODE_AND_DOC_REVIEW_MODEL=anthropic/claude-sonnet-4-5-20250929
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 │ │ Code & Doc Reviewer │ │ │
647
- │ │ │ Auditor │ │ (defects + documentation) │ │ │
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** | `scope_identification` | Identifies code scopes (frontend, backend, infra, microservice in mono repo, etc...) |
692
- | **CodeAndDocReviewSignature** | `code_and_doc_review` | Detects verified bugs, security vulnerabilities, and stale/wrong documentation in a single pass |
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 |
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "codespy-ai"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "Code review agent powered by DSPy"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,3 +1,3 @@
1
1
  """codespy - Code review agent powered by DSPy."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.3.2"
@@ -206,8 +206,8 @@ class SignatureContext:
206
206
  3. Summing costs/tokens from new entries
207
207
 
208
208
  Usage:
209
- with SignatureContext("code_and_doc_review", cost_tracker):
210
- # All LLM calls here will be attributed to code_and_doc_review
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., code_and_doc_review, supply_chain)")
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.code_and_doc_reviewer import CodeAndDocReviewer
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
- "CodeAndDocReviewer",
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)