Skip to content

scorer

scorer

AI-powered code review scorer for Marianne.

Provides automated quality assessment of code changes after batch execution. The reviewer analyzes git diffs and produces a quality score (0-100) with detailed feedback on issues and suggestions.

Score Components: - Code Quality (30%): Complexity, duplication, pattern adherence - Test Coverage (25%): New code tested, edge cases - Security (25%): No secrets, validation, error handling - Documentation (20%): Public API docs, complex logic explained

Classes

ReviewIssue dataclass

ReviewIssue(severity, category, description, suggestion=None)

A single issue found during code review.

Functions
to_dict
to_dict()

Convert to dictionary.

Source code in src/marianne/review/scorer.py
def to_dict(self) -> dict[str, Any]:
    """Convert to dictionary."""
    return {
        "severity": self.severity,
        "category": self.category,
        "description": self.description,
        "suggestion": self.suggestion,
    }

AIReviewResult dataclass

AIReviewResult(score, components=dict(), issues=list(), summary='', raw_response='', error=None)

Result of an AI code review.

Attributes
passed property
passed

Check if review passed minimum threshold (60).

high_quality property
high_quality

Check if review achieved target score (80+).

Functions
to_dict
to_dict()

Convert to dictionary.

Source code in src/marianne/review/scorer.py
def to_dict(self) -> dict[str, Any]:
    """Convert to dictionary."""
    return {
        "score": self.score,
        "components": self.components,
        "issues": [i.to_dict() for i in self.issues],
        "summary": self.summary,
        "passed": self.passed,
        "high_quality": self.high_quality,
        "error": self.error,
    }

GitDiffProvider

GitDiffProvider(since_commit=None)

Gets diffs using git commands.

Initialize with optional since commit.

Parameters:

Name Type Description Default
since_commit str | None

Commit hash to diff from. If None, uses staged + unstaged.

None
Source code in src/marianne/review/scorer.py
def __init__(self, since_commit: str | None = None) -> None:
    """Initialize with optional since commit.

    Args:
        since_commit: Commit hash to diff from. If None, uses staged + unstaged.
    """
    self.since_commit = since_commit
Functions
get_diff
get_diff(workspace)

Get git diff for the workspace.

Parameters:

Name Type Description Default
workspace Path

Directory to get diff from.

required

Returns:

Type Description
str

Git diff as string, or empty string if not a git repo.

Source code in src/marianne/review/scorer.py
def get_diff(self, workspace: Path) -> str:
    """Get git diff for the workspace.

    Args:
        workspace: Directory to get diff from.

    Returns:
        Git diff as string, or empty string if not a git repo.
    """
    try:
        if self.since_commit:
            # Diff from specific commit
            # Use "--" separator to prevent flag injection from since_commit
            result = subprocess.run(
                ["git", "diff", self.since_commit, "HEAD", "--"],
                cwd=str(workspace),
                capture_output=True,
                text=True,
                timeout=30,
            )
        else:
            # Get both staged and unstaged changes
            staged = subprocess.run(
                ["git", "diff", "--cached"],
                cwd=str(workspace),
                capture_output=True,
                text=True,
                timeout=30,
            )
            unstaged = subprocess.run(
                ["git", "diff"],
                cwd=str(workspace),
                capture_output=True,
                text=True,
                timeout=30,
            )
            return staged.stdout + unstaged.stdout

        return result.stdout
    except subprocess.TimeoutExpired:
        _logger.warning("Git diff timed out")
        return ""
    except FileNotFoundError:
        raise RuntimeError(
            "Git is not installed or not found on PATH. "
            "Git is required for code review scoring."
        ) from None
    except (subprocess.SubprocessError, OSError) as e:
        _logger.warning("git_diff_error", error=str(e))
        return ""

AIReviewer

AIReviewer(backend, config, diff_provider=None)

Performs AI-powered code review using a backend.

Uses the same backend as Marianne execution to send the diff for review and parse the scoring response.

Initialize reviewer.

Parameters:

Name Type Description Default
backend Backend

Execution backend for AI calls.

required
config AIReviewConfig

AI review configuration.

required
diff_provider GitDiffProvider | None

Provider for git diffs. Defaults to GitDiffProvider.

None
Source code in src/marianne/review/scorer.py
def __init__(
    self,
    backend: "Backend",
    config: "AIReviewConfig",
    diff_provider: GitDiffProvider | None = None,
) -> None:
    """Initialize reviewer.

    Args:
        backend: Execution backend for AI calls.
        config: AI review configuration.
        diff_provider: Provider for git diffs. Defaults to GitDiffProvider.
    """
    self.backend = backend
    self.config = config
    self.diff_provider = diff_provider or GitDiffProvider()
Functions
review async
review(workspace)

Perform AI review on workspace changes.

Parameters:

Name Type Description Default
workspace Path

Directory to review.

required

Returns:

Type Description
AIReviewResult

AIReviewResult with score and feedback.

Source code in src/marianne/review/scorer.py
async def review(self, workspace: Path) -> AIReviewResult:
    """Perform AI review on workspace changes.

    Args:
        workspace: Directory to review.

    Returns:
        AIReviewResult with score and feedback.
    """
    if not self.config.enabled:
        return AIReviewResult(
            score=100,
            summary="AI review disabled",
        )

    # Get the diff
    diff = self.diff_provider.get_diff(workspace)

    if not diff or not diff.strip():
        _logger.debug("No diff to review")
        return AIReviewResult(
            score=100,
            summary="No changes to review",
        )

    # Truncate very large diffs
    max_diff_chars = 50000
    if len(diff) > max_diff_chars:
        diff = diff[:max_diff_chars] + "\n\n... [diff truncated] ..."

    # Build review prompt
    prompt_template = self.config.review_prompt_template or DEFAULT_REVIEW_PROMPT
    prompt = prompt_template.format(diff=diff)

    try:
        # Execute review using backend
        result = await self.backend.execute(prompt)

        if result.success and result.output:
            return self._parse_review_response(result.output)
        else:
            return AIReviewResult(
                score=0,
                error=result.error_message or "Review execution failed",
                summary="Failed to execute review",
            )
    except (OSError, TimeoutError, RuntimeError) as e:
        _logger.error("ai_review_failed", error=str(e))
        return AIReviewResult(
            score=0,
            error=str(e),
            summary="Review failed with exception",
        )
evaluate_result
evaluate_result(result)

Evaluate review result against config thresholds.

Parameters:

Name Type Description Default
result AIReviewResult

Review result to evaluate.

required

Returns:

Type Description
bool

Tuple of (passed, message).

str

passed is True if score >= min_score.

Source code in src/marianne/review/scorer.py
def evaluate_result(
    self, result: AIReviewResult
) -> tuple[bool, str]:
    """Evaluate review result against config thresholds.

    Args:
        result: Review result to evaluate.

    Returns:
        Tuple of (passed, message).
        passed is True if score >= min_score.
    """
    if result.score >= self.config.target_score:
        return True, f"High quality ({result.score}/100): {result.summary}"
    elif result.score >= self.config.min_score:
        return True, f"Acceptable ({result.score}/100): {result.summary}"
    else:
        return False, f"Below threshold ({result.score}/100): {result.summary}"

Functions