Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1289,12 +1289,30 @@ async def review(self, context: PRContext) -> PRReviewResult:
f"{len(filtered_findings)} filtered"
)

# No confidence routing - validation is binary via finding-validator
unique_findings = validated_findings
logger.info(f"[PRReview] Final findings: {len(unique_findings)} validated")
# Separate active findings (drive verdict) from dismissed (shown in UI only)
active_findings = []
dismissed_findings = []
for f in validated_findings:
if f.validation_status == "dismissed_false_positive":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider extracting "dismissed_false_positive" to a constant.

The string "dismissed_false_positive" appears in 5 locations across this file (lines 1296, 1976, 2174, 2210, 2240). A single constant (e.g., VALIDATION_DISMISSED = "dismissed_false_positive") would eliminate risk of typo-induced bugs and make future status changes easier.

Suggested approach
# At module level or in models.py alongside PRReviewFinding
VALIDATION_STATUS_DISMISSED = "dismissed_false_positive"
VALIDATION_STATUS_CONFIRMED = "confirmed_valid"
VALIDATION_STATUS_NEEDS_HUMAN = "needs_human_review"

Then replace all bare string comparisons with the constants.

Also applies to: 2174-2174, 2240-2240

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/runners/github/services/parallel_orchestrator_reviewer.py` at
line 1296, Extract the magic string "dismissed_false_positive" into a
module-level constant (e.g., VALIDATION_STATUS_DISMISSED =
"dismissed_false_positive") and replace all direct comparisons of
f.validation_status == "dismissed_false_positive" (and similar string literals
used elsewhere in this file) with that constant; update any related status
checks in this module (occurrences near f.validation_status, and other places
where the same literal appears) so they reference VALIDATION_STATUS_DISMISSED to
avoid duplication and typos, and consider adding complementary constants like
VALIDATION_STATUS_CONFIRMED and VALIDATION_STATUS_NEEDS_HUMAN if similar
literals are present.

dismissed_findings.append(f)
else:
active_findings.append(f)

safe_print(
f"[ParallelOrchestrator] Final: {len(active_findings)} active, "
f"{len(dismissed_findings)} disputed by validator",
flush=True,
)
logger.info(
f"[PRReview] Final findings: {len(active_findings)} active, "
f"{len(dismissed_findings)} disputed"
)

# All findings (active + dismissed) go in the result for UI display
all_review_findings = validated_findings
logger.info(
f"[ParallelOrchestrator] Review complete: {len(unique_findings)} findings"
f"[ParallelOrchestrator] Review complete: {len(all_review_findings)} findings "
f"({len(active_findings)} active, {len(dismissed_findings)} disputed)"
)

# Fetch CI status for verdict consideration
Expand All @@ -1304,9 +1322,9 @@ async def review(self, context: PRContext) -> PRReviewResult:
f"{ci_status.get('failing', 0)} failing, {ci_status.get('pending', 0)} pending"
)

# Generate verdict (includes merge conflict check, branch-behind check, and CI status)
# Generate verdict from ACTIVE findings only (dismissed don't affect verdict)
verdict, verdict_reasoning, blockers = self._generate_verdict(
unique_findings,
active_findings,
has_merge_conflicts=context.has_merge_conflicts,
merge_state_status=context.merge_state_status,
ci_status=ci_status,
Expand All @@ -1317,7 +1335,7 @@ async def review(self, context: PRContext) -> PRReviewResult:
verdict=verdict,
verdict_reasoning=verdict_reasoning,
blockers=blockers,
findings=unique_findings,
findings=all_review_findings,
agents_invoked=agents_invoked,
)

Expand Down Expand Up @@ -1362,7 +1380,7 @@ async def review(self, context: PRContext) -> PRReviewResult:
pr_number=context.pr_number,
repo=self.config.repo,
success=True,
findings=unique_findings,
findings=all_review_findings,
summary=summary,
overall_status=overall_status,
verdict=verdict,
Expand Down Expand Up @@ -1937,12 +1955,38 @@ async def _validate_findings(
validated_findings.append(finding)

elif validation.validation_status == "dismissed_false_positive":
# Dismiss - do not include
dismissed_count += 1
logger.info(
f"[PRReview] Dismissed {finding.id} as false positive: "
f"{validation.explanation[:100]}"
)
# Protect cross-validated findings from dismissal —
# if multiple specialists independently found the same issue,
# a single validator should not override that consensus
if finding.cross_validated:
finding.validation_status = "confirmed_valid"
finding.validation_evidence = validation.code_evidence
finding.validation_explanation = (
f"[Auto-kept: cross-validated by {len(finding.source_agents)} agents] "
f"{validation.explanation}"
)
validated_findings.append(finding)
safe_print(
f"[FindingValidator] Kept cross-validated finding '{finding.title}' "
f"despite dismissal (agents={finding.source_agents})",
flush=True,
)
else:
# Keep finding but mark as dismissed (user can see it in UI)
finding.validation_status = "dismissed_false_positive"
finding.validation_evidence = validation.code_evidence
finding.validation_explanation = validation.explanation
validated_findings.append(finding)
dismissed_count += 1
safe_print(
f"[FindingValidator] Disputed '{finding.title}': "
f"{validation.explanation} (file={finding.file}:{finding.line})",
flush=True,
)
logger.info(
f"[PRReview] Disputed {finding.id}: "
f"{validation.explanation[:200]}"
)
Comment on lines 1957 to +1989
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Cross-validation protection and dismissed-finding retention look correct.

Two downstream issues to address:

  1. Misleading log at line 2001: validated_findings now includes dismissed findings, so {len(validated_findings)} valid, {dismissed_count} dismissed double-counts dismissed items. Consider {len(validated_findings) - dismissed_count} confirmed, {dismissed_count} dismissed.

  2. Raw status string in summary (line 2205-2208): _generate_summary doesn't map "dismissed_false_positive" in the status_label dict, so the GitHub-rendered summary will display the raw string "dismissed_false_positive". Add a mapping entry.

Proposed fixes

Fix 1 — Accurate log at line 2000:

         logger.info(
-            f"[PRReview] Validation complete: {len(validated_findings)} valid, "
+            f"[PRReview] Validation complete: {len(validated_findings)} total "
+            f"({len(validated_findings) - dismissed_count} confirmed, "
             f"{dismissed_count} dismissed, {needs_human_count} need human review"
         )

Fix 2 — Add label mapping at line 2205:

                     status_label = {
                         "confirmed_valid": "Confirmed",
+                        "dismissed_false_positive": "Disputed by Validator",
                         "needs_human_review": "Needs human review",
                     }.get(f.validation_status, f.validation_status)
🤖 Prompt for AI Agents
In `@apps/backend/runners/github/services/parallel_orchestrator_reviewer.py`
around lines 1957 - 1989, The log that reports counts is double-counting
dismissed items because validated_findings includes both confirmed and dismissed
entries; update the logger.info message that currently formats
"{len(validated_findings)} valid, {dismissed_count} dismissed" to compute
confirmed as len(validated_findings) - dismissed_count (use validated_findings
and dismissed_count variables) so it logs "{confirmed} confirmed,
{dismissed_count} dismissed". Also update the _generate_summary implementation's
status_label dictionary to include a human-friendly mapping for the
"dismissed_false_positive" status (e.g., "Dismissed (False Positive)") so the
GitHub-rendered summary shows a readable label instead of the raw status string.


elif validation.validation_status == "needs_human_review":
# Keep but flag
Expand Down Expand Up @@ -2127,11 +2171,16 @@ def _generate_summary(
sev = f.severity.value
emoji = severity_emoji.get(sev, "⚪")

is_disputed = f.validation_status == "dismissed_false_positive"

# Finding header with location
line_range = f"L{f.line}"
if f.end_line and f.end_line != f.line:
line_range = f"L{f.line}-L{f.end_line}"
lines.append(f"#### {emoji} [{sev.upper()}] {f.title}")
if is_disputed:
lines.append(f"#### ⚪ [DISPUTED] ~~{f.title}~~")
else:
lines.append(f"#### {emoji} [{sev.upper()}] {f.title}")
lines.append(f"**File:** `{f.file}` ({line_range})")

# Cross-validation badge
Expand Down Expand Up @@ -2161,6 +2210,7 @@ def _generate_summary(
status_label = {
"confirmed_valid": "Confirmed",
"needs_human_review": "Needs human review",
"dismissed_false_positive": "Disputed by validator",
}.get(f.validation_status, f.validation_status)
lines.append("")
lines.append(f"**Validation:** {status_label}")
Expand All @@ -2182,18 +2232,27 @@ def _generate_summary(

lines.append("")

# Findings count summary
# Findings count summary (exclude dismissed from active count)
active_count = 0
dismissed_count = 0
by_severity: dict[str, int] = {}
for f in findings:
if f.validation_status == "dismissed_false_positive":
dismissed_count += 1
continue
active_count += 1
sev = f.severity.value
by_severity[sev] = by_severity.get(sev, 0) + 1
summary_parts = []
for sev in ["critical", "high", "medium", "low"]:
if sev in by_severity:
summary_parts.append(f"{by_severity[sev]} {sev}")
lines.append(
f"**Total:** {len(findings)} finding(s) ({', '.join(summary_parts)})"
count_text = (
f"**Total:** {active_count} finding(s) ({', '.join(summary_parts)})"
)
if dismissed_count > 0:
count_text += f" + {dismissed_count} disputed"
lines.append(count_text)
lines.append("")

lines.append("---")
Expand Down
8 changes: 8 additions & 0 deletions apps/frontend/src/main/ipc-handlers/github/pr-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,10 @@ export interface PRReviewFinding {
endLine?: number;
suggestedFix?: string;
fixable: boolean;
validationStatus?: "confirmed_valid" | "dismissed_false_positive" | "needs_human_review" | null;
validationExplanation?: string;
sourceAgents?: string[];
crossValidated?: boolean;
}

/**
Expand Down Expand Up @@ -1341,6 +1345,10 @@ function getReviewResult(project: Project, prNumber: number): PRReviewResult | n
endLine: f.end_line,
suggestedFix: f.suggested_fix,
fixable: f.fixable ?? false,
validationStatus: f.validation_status ?? null,
validationExplanation: f.validation_explanation ?? undefined,
sourceAgents: f.source_agents ?? [],
crossValidated: f.cross_validated ?? false,
})) ?? [],
summary: data.summary ?? "",
overallStatus: data.overall_status ?? "comment",
Expand Down
4 changes: 4 additions & 0 deletions apps/frontend/src/preload/api/modules/github-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,10 @@ export interface PRReviewFinding {
endLine?: number;
suggestedFix?: string;
fixable: boolean;
validationStatus?: 'confirmed_valid' | 'dismissed_false_positive' | 'needs_human_review' | null;
validationExplanation?: string;
sourceAgents?: string[];
crossValidated?: boolean;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface FindingItemProps {
finding: PRReviewFinding;
selected: boolean;
posted?: boolean;
disputed?: boolean;
onToggle: () => void;
}

Expand All @@ -33,7 +34,7 @@ function getCategoryTranslationKey(category: string): string {
return categoryMap[category.toLowerCase()] || category;
}

export function FindingItem({ finding, selected, posted = false, onToggle }: FindingItemProps) {
export function FindingItem({ finding, selected, posted = false, disputed = false, onToggle }: FindingItemProps) {
const { t } = useTranslation('common');
const CategoryIcon = getCategoryIcon(finding.category);

Expand All @@ -45,8 +46,9 @@ export function FindingItem({ finding, selected, posted = false, onToggle }: Fin
<div
className={cn(
"rounded-lg border bg-background p-3 space-y-2 transition-colors",
selected && !posted && "ring-2 ring-primary/50",
posted && "opacity-60"
selected && !posted && !disputed && "ring-2 ring-primary/50",
selected && disputed && "ring-2 ring-purple-500/50",
(posted || (disputed && !selected)) && "opacity-60"
)}
>
{/* Finding Header */}
Expand All @@ -72,13 +74,28 @@ export function FindingItem({ finding, selected, posted = false, onToggle }: Fin
{t('prReview.posted')}
</Badge>
)}
{disputed && (
<Badge variant="outline" className="text-xs shrink-0 bg-purple-500/10 text-purple-500 border-purple-500/30">
{t('prReview.disputed')}
</Badge>
)}
{finding.crossValidated && finding.sourceAgents && finding.sourceAgents.length > 1 && (
<Badge variant="outline" className="text-xs shrink-0 bg-green-500/10 text-green-500 border-green-500/30">
{t('prReview.crossValidatedBy', { count: finding.sourceAgents.length })}
</Badge>
)}
<span className="font-medium text-sm break-words">
{finding.title}
</span>
</div>
<p className="text-sm text-muted-foreground break-words">
{finding.description}
</p>
{disputed && finding.validationExplanation && (
<p className="text-xs text-purple-500/80 italic break-words">
{finding.validationExplanation}
</p>
)}
<div className="text-xs text-muted-foreground">
<code className="bg-muted px-1 py-0.5 rounded break-all">
{finding.file}:{finding.line}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import type { PRReviewFinding } from '../hooks/useGitHubPRs';
interface FindingsSummaryProps {
findings: PRReviewFinding[];
selectedCount: number;
disputedCount?: number;
}

export function FindingsSummary({ findings, selectedCount }: FindingsSummaryProps) {
export function FindingsSummary({ findings, selectedCount, disputedCount = 0 }: FindingsSummaryProps) {
const { t } = useTranslation('common');

// Count findings by severity
Expand Down Expand Up @@ -46,6 +47,11 @@ export function FindingsSummary({ findings, selectedCount }: FindingsSummaryProp
{counts.low} {t('prReview.severity.low')}
</Badge>
)}
{disputedCount > 0 && (
<Badge variant="outline" className="bg-purple-500/10 text-purple-500 border-purple-500/30">
{disputedCount} {t('prReview.disputed')}
</Badge>
)}
</div>
<span className="text-xs text-muted-foreground">
{t('prReview.selectedOfTotal', { selected: selectedCount, total: counts.total })}
Expand Down
Loading
Loading