Skip to content

Latest commit

 

History

History
118 lines (88 loc) · 3.89 KB

File metadata and controls

118 lines (88 loc) · 3.89 KB

Architecture

Why a CLI, not a web app?

A web app requires a server, a database, a job queue, and auth — none of which add value to a code review tool. A CLI is stateless: you run it, it does the work, you read the output. It is also easier to integrate into a local workflow (pre-push hook, CI pipeline) than a hosted service.

The tradeoff: no persistent review history, no multi-user dashboard. For a portfolio project demonstrating LLM integration and clean Python design, the CLI is the right scope.


Why not parse the AST?

Tree-sitter or ast would give us precise symbol names, call graphs, and type information. It would also add:

  • Language-specific parsers for every file type in the PR
  • ~300 lines of visitor code to extract the "interesting" nodes
  • Fragility when the PR contains syntax errors (AST parse fails)

Claude already understands Python syntax from pre-training. Sending raw diff text and letting the LLM reason about it is simpler and generalises to every language without extra code. The only thing AST would meaningfully add is precise line numbers — and the unified diff already gives us those via the @@ hunk headers.

Decision: send raw unified diff per file; ask Claude to cite line numbers from the diff hunk context.


Chunking strategy

A GitHub PR can contain hundreds of files. Sending the entire diff in one prompt risks:

  1. Exceeding Claude's context window (200k tokens, but long prompts are slow and expensive)
  2. Diluting attention — Claude gives shallower comments on a 50-file diff than on 5 files

Strategy: one API call per file. reviewer.py iterates over files returned by github_client.py and calls Claude once per file diff. Results are collected and passed to formatter.py.

Tradeoff: N API calls instead of 1. For a typical PR (5–20 files) this is fast enough. A future optimisation is to pack small files into a single prompt until a token budget is hit.


Prompt design (your TODO — prompt.py)

The prompt has two parts:

System prompt — sets Claude's persona and output contract:

  • Role: "You are an expert Python code reviewer."
  • Output format: JSON with a fixed schema (issues list + summary)
  • Constraints: cite file name and line number; severity must be HIGH/MEDIUM/LOW

User prompt — provides the diff for one file:

  • File name and language
  • The unified diff text
  • PR title and description for context

The JSON contract is critical. Without it, Claude returns free-text prose that is hard to parse reliably. Asking for JSON + validating with Pydantic (validator.py) catches cases where Claude invents new fields or omits required ones.

Suggested schema

{
  "summary": "One paragraph overall assessment",
  "issues": [
    {
      "severity": "HIGH | MEDIUM | LOW",
      "file": "path/to/file.py",
      "line": 42,
      "title": "Short description",
      "explanation": "Why this is a problem",
      "suggestion": "How to fix it"
    }
  ]
}

Validation strategy (your TODO — validator.py)

Claude occasionally:

  • Returns valid JSON wrapped in a markdown code fence (json ... )
  • Omits optional fields
  • Uses a severity string not in the enum ("CRITICAL" instead of "HIGH")

validator.py should:

  1. Strip markdown fences before parsing
  2. Use Pydantic model_validate() with strict=False to coerce types
  3. Map unknown severity values to "MEDIUM" as a safe fallback
  4. Raise a typed ValidationError (not a raw json.JSONDecodeError) so reviewer.py can log a warning and continue rather than crashing

Error handling philosophy

  • Network errors (GitHub API, Claude API): surface a clean message with the HTTP status code; do not print a stack trace to the user
  • Validation errors: log a warning and continue (skip the file rather than abort the whole review)
  • Auth errors (missing token): fail fast with an actionable message pointing to the env var name