This guide documents the Reviews feature end to end: the pages contributors
use, the REVIEW_* environment settings that bound every run, and the JSON API
that powers the UI. It is the companion to the feature summary in
README.md and the configuration table there.
An AI review never merges, closes, approves, or otherwise modifies a pull request, and it never sends more repository text to the model than the configured bounds allow. Everything is owner-scoped: a review, its findings, and a project's review configuration are only visible to the user who owns them.
The reviews blueprint (app/reviews/routes.py) serves four HTML pages:
| Page | Route | Purpose |
|---|---|---|
| Review history | GET /reviews/ |
Quality-metrics strip plus the list of the user's reviews (filter by source/kind/status). |
| Review detail | GET /reviews/<review_id> |
The stored summary and structured findings for one review; findings can be marked addressed and filtered by severity/category. |
| Project reviews | GET /reviews/projects/<project_id> |
Run a new quality, security, or tests review over an imported project and see that project's history. |
| Project review config | GET /reviews/projects/<project_id>/config |
Per-project review configuration (kinds, severity threshold, languages, focus areas, bounds). |
The history list, detail view, and metrics strip are rendered client-side by
app/static/js/reviews.js, which calls the JSON API below. Pages are protected
by @login_required; the underlying APIs are owner-scoped (a review or
project owned by another user returns 404).
- Import a project (GitHub repo or archive) under Workspaces. Project
reviews require the project to finish indexing (
status == "ready"); otherwise the run is rejected with409. - Configure the project (optional) on the project review config page — pick the enabled kinds, severity threshold, language filter, and focus areas.
- Run a review from the project reviews page. A review row is created with
status="running", the AI service runs, and the result is persisted as a summary plusReviewFindingrows. A failed run is stored withstatus="failed"and anerror_message— never silently dropped. - Triage findings on the review detail page: mark a finding addressed, or filter by severity/category/confidence. The quality dashboard recomputes its metrics from the stored rows only.
Findings use a normalized vocabulary (see app/models/review_finding.py):
- Severity:
critical,high,medium,low,informational. - Confidence:
confirmed,potential,suggestion. Confirmed findings are the ones the code proves; everything else is labelled[SUGGESTION]in the UI (confidence_label). - Category: depends on the review kind —
prreviews use bug/security/logic/ performance/…;qualityuses readability/complexity/duplication/dead-code/…;securityuses authentication/authorization/injection/secrets/…;testsuses coverage-gap/missing-assertion/flaky-test/….
A finding carries a file path and line only — never raw repository content.
Parsed model output that cannot be validated is rejected, never coerced:
- Unknown severity, category, or confidence values are dropped (no defaulting
to
medium/other/suggestion). - Findings need a string explanation; non-integer, boolean, zero, negative, or oversized lines are dropped.
- File paths must be syntactically valid (no traversal, backslashes, or
control characters) and, when the review context supplies a known-file
allow-list, must name a file from that context — invented paths and
files_affectedentries are dropped. - Coverage percentages and coverage-with-a-number claims are dropped from
summaries, findings, and recommendations: the service has no coverage
runner and never reports measured percentages. Metrics are computed only
from persisted
Review/ReviewFindingrows, never from model text. - Non-object payloads, non-list
findings, non-dictsummary, and non-string summary items are dropped; findings are capped atREVIEW_MAX_FINDINGS.
All review behaviour is environment-driven and documented in
.env.example. These are the REVIEW_* settings:
| Variable | Default | Description |
|---|---|---|
REVIEW_MAX_FILES |
40 |
Maximum changed files analyzed in one pull-request review. |
REVIEW_MAX_CONTEXT_CHARS |
40000 |
Maximum diff/repository context (characters) sent to the model per review. |
REVIEW_MAX_FINDINGS |
100 |
Maximum findings stored per review. |
REVIEW_KINDS |
quality,security,tests |
Default enabled project review kinds (comma-separated). |
REVIEW_SEVERITY_THRESHOLD |
low |
Minimum severity stored for project reviews (critical/high/medium/low/informational). |
Per-project configuration can narrow the kinds, severity threshold, languages,
focus areas, and bounds, but it can never exceed the REVIEW_* caps above.
Every route below is a JSON API served by app/reviews/routes.py and requires a
logged-in user. Errors are JSON bodies of the form {"error": "<message>"}.
GitHub errors reuse the sanitized {"error", "kind"} payload from
app/services/github.py — raw GitHub messages, tokens, and headers are never
returned.
GET /reviews/api/reviews — optional query params source (github_pr /
project), kind, status, and project_id. Results are paginated with
page (default 1) and per_page (default 20, capped at 100), and ordered
newest first with id breaking created_at ties so paging is stable. The
response is a page envelope, not a bare array.
{
"items": [
{
"id": 12,
"source": "project",
"kind": "security",
"status": "completed",
"findings_count": 3,
"summary": {"overview": "..."},
"created_at": "2026-01-01T00:00:00+00:00"
}
],
"total": 42,
"page": 1,
"per_page": 20,
"total_pages": 3,
"has_next": true,
"has_prev": false
}POST /reviews/api/reviews — body selects the source.
Project review:
{"source": "project", "project_id": 3, "kind": "quality"}project_idmust be owned by the caller and indexed, else409.kindmust be one ofquality/security/tests, else400.- Rejected with
400when the project's config disables reviewing or disables that kind. - Returns
201with the created review (status="completed"or"failed").
Pull-request review:
{"source": "github_pr", "repo": "owner/name", "pr_number": 42, "project_id": 3}repomust be a validowner/name;pr_numberis required.- Requires a connected GitHub account; the PR and its files are fetched with the
user's own token (
400/502on GitHub errors). project_idis optional and only links the review to a project.- Returns
201with the created review (kind="pr").
GET /reviews/api/reviews/<review_id> — the review plus categories, the
category vocabulary for its kind (used to build the detail-page filter).
DELETE /reviews/api/reviews/<review_id> — deletes the review and its findings
(owner only). Returns {"ok": true}.
GET /reviews/api/reviews/<review_id>/findings — optional filters severity,
category, confidence, and addressed (0/1). Returns findings ordered by
severity then id.
PATCH /reviews/api/reviews/findings/<finding_id> — body {"addressed": true}.
Returns the updated finding.
GET /reviews/api/projects/<project_id>/config — the stored config merged with
application defaults.
PATCH /reviews/api/projects/<project_id>/config — accepts any of kinds,
severity_threshold, languages, testing_focus, security_focus,
performance_focus, max_files (1–200), max_context_chars (2000–200000), and
enabled. Invalid values are rejected with 400 and the row is rolled back.
{
"kinds": "quality,tests",
"severity_threshold": "medium",
"languages": "python,typescript",
"max_files": 25,
"max_context_chars": 40000,
"enabled": true
}GET /reviews/api/metrics — aggregate metrics computed strictly from stored
reviews and findings. Scope with workspace_id or project_id; with neither it
returns metrics for the current user. The payload includes total_reviews,
by_status, by_kind, by_source, a findings breakdown (totals by severity,
category, confidence, file, addressed/high-risk counts), and
findings_trend (per-review open vs addressed counts, oldest first).