@@ -1289,12 +1289,30 @@ async def review(self, context: PRContext) -> PRReviewResult:
12891289 f"{ len (filtered_findings )} filtered"
12901290 )
12911291
1292- # No confidence routing - validation is binary via finding-validator
1293- unique_findings = validated_findings
1294- logger .info (f"[PRReview] Final findings: { len (unique_findings )} validated" )
1292+ # Separate active findings (drive verdict) from dismissed (shown in UI only)
1293+ active_findings = []
1294+ dismissed_findings = []
1295+ for f in validated_findings :
1296+ if f .validation_status == "dismissed_false_positive" :
1297+ dismissed_findings .append (f )
1298+ else :
1299+ active_findings .append (f )
12951300
1301+ safe_print (
1302+ f"[ParallelOrchestrator] Final: { len (active_findings )} active, "
1303+ f"{ len (dismissed_findings )} disputed by validator" ,
1304+ flush = True ,
1305+ )
1306+ logger .info (
1307+ f"[PRReview] Final findings: { len (active_findings )} active, "
1308+ f"{ len (dismissed_findings )} disputed"
1309+ )
1310+
1311+ # All findings (active + dismissed) go in the result for UI display
1312+ all_review_findings = validated_findings
12961313 logger .info (
1297- f"[ParallelOrchestrator] Review complete: { len (unique_findings )} findings"
1314+ f"[ParallelOrchestrator] Review complete: { len (all_review_findings )} findings "
1315+ f"({ len (active_findings )} active, { len (dismissed_findings )} disputed)"
12981316 )
12991317
13001318 # Fetch CI status for verdict consideration
@@ -1304,9 +1322,9 @@ async def review(self, context: PRContext) -> PRReviewResult:
13041322 f"{ ci_status .get ('failing' , 0 )} failing, { ci_status .get ('pending' , 0 )} pending"
13051323 )
13061324
1307- # Generate verdict (includes merge conflict check, branch-behind check, and CI status )
1325+ # Generate verdict from ACTIVE findings only (dismissed don't affect verdict )
13081326 verdict , verdict_reasoning , blockers = self ._generate_verdict (
1309- unique_findings ,
1327+ active_findings ,
13101328 has_merge_conflicts = context .has_merge_conflicts ,
13111329 merge_state_status = context .merge_state_status ,
13121330 ci_status = ci_status ,
@@ -1317,7 +1335,7 @@ async def review(self, context: PRContext) -> PRReviewResult:
13171335 verdict = verdict ,
13181336 verdict_reasoning = verdict_reasoning ,
13191337 blockers = blockers ,
1320- findings = unique_findings ,
1338+ findings = all_review_findings ,
13211339 agents_invoked = agents_invoked ,
13221340 )
13231341
@@ -1362,7 +1380,7 @@ async def review(self, context: PRContext) -> PRReviewResult:
13621380 pr_number = context .pr_number ,
13631381 repo = self .config .repo ,
13641382 success = True ,
1365- findings = unique_findings ,
1383+ findings = all_review_findings ,
13661384 summary = summary ,
13671385 overall_status = overall_status ,
13681386 verdict = verdict ,
@@ -1937,12 +1955,38 @@ async def _validate_findings(
19371955 validated_findings .append (finding )
19381956
19391957 elif validation .validation_status == "dismissed_false_positive" :
1940- # Dismiss - do not include
1941- dismissed_count += 1
1942- logger .info (
1943- f"[PRReview] Dismissed { finding .id } as false positive: "
1944- f"{ validation .explanation [:100 ]} "
1945- )
1958+ # Protect cross-validated findings from dismissal —
1959+ # if multiple specialists independently found the same issue,
1960+ # a single validator should not override that consensus
1961+ if finding .cross_validated :
1962+ finding .validation_status = "confirmed_valid"
1963+ finding .validation_evidence = validation .code_evidence
1964+ finding .validation_explanation = (
1965+ f"[Auto-kept: cross-validated by { len (finding .source_agents )} agents] "
1966+ f"{ validation .explanation } "
1967+ )
1968+ validated_findings .append (finding )
1969+ safe_print (
1970+ f"[FindingValidator] Kept cross-validated finding '{ finding .title } ' "
1971+ f"despite dismissal (agents={ finding .source_agents } )" ,
1972+ flush = True ,
1973+ )
1974+ else :
1975+ # Keep finding but mark as dismissed (user can see it in UI)
1976+ finding .validation_status = "dismissed_false_positive"
1977+ finding .validation_evidence = validation .code_evidence
1978+ finding .validation_explanation = validation .explanation
1979+ validated_findings .append (finding )
1980+ dismissed_count += 1
1981+ safe_print (
1982+ f"[FindingValidator] Disputed '{ finding .title } ': "
1983+ f"{ validation .explanation } (file={ finding .file } :{ finding .line } )" ,
1984+ flush = True ,
1985+ )
1986+ logger .info (
1987+ f"[PRReview] Disputed { finding .id } : "
1988+ f"{ validation .explanation [:200 ]} "
1989+ )
19461990
19471991 elif validation .validation_status == "needs_human_review" :
19481992 # Keep but flag
@@ -2127,11 +2171,16 @@ def _generate_summary(
21272171 sev = f .severity .value
21282172 emoji = severity_emoji .get (sev , "⚪" )
21292173
2174+ is_disputed = f .validation_status == "dismissed_false_positive"
2175+
21302176 # Finding header with location
21312177 line_range = f"L{ f .line } "
21322178 if f .end_line and f .end_line != f .line :
21332179 line_range = f"L{ f .line } -L{ f .end_line } "
2134- lines .append (f"#### { emoji } [{ sev .upper ()} ] { f .title } " )
2180+ if is_disputed :
2181+ lines .append (f"#### ⚪ [DISPUTED] ~~{ f .title } ~~" )
2182+ else :
2183+ lines .append (f"#### { emoji } [{ sev .upper ()} ] { f .title } " )
21352184 lines .append (f"**File:** `{ f .file } ` ({ line_range } )" )
21362185
21372186 # Cross-validation badge
@@ -2161,6 +2210,7 @@ def _generate_summary(
21612210 status_label = {
21622211 "confirmed_valid" : "Confirmed" ,
21632212 "needs_human_review" : "Needs human review" ,
2213+ "dismissed_false_positive" : "Disputed by validator" ,
21642214 }.get (f .validation_status , f .validation_status )
21652215 lines .append ("" )
21662216 lines .append (f"**Validation:** { status_label } " )
@@ -2182,18 +2232,27 @@ def _generate_summary(
21822232
21832233 lines .append ("" )
21842234
2185- # Findings count summary
2235+ # Findings count summary (exclude dismissed from active count)
2236+ active_count = 0
2237+ dismissed_count = 0
21862238 by_severity : dict [str , int ] = {}
21872239 for f in findings :
2240+ if f .validation_status == "dismissed_false_positive" :
2241+ dismissed_count += 1
2242+ continue
2243+ active_count += 1
21882244 sev = f .severity .value
21892245 by_severity [sev ] = by_severity .get (sev , 0 ) + 1
21902246 summary_parts = []
21912247 for sev in ["critical" , "high" , "medium" , "low" ]:
21922248 if sev in by_severity :
21932249 summary_parts .append (f"{ by_severity [sev ]} { sev } " )
2194- lines . append (
2195- f"**Total:** { len ( findings ) } finding(s) ({ ', ' .join (summary_parts )} )"
2250+ count_text = (
2251+ f"**Total:** { active_count } finding(s) ({ ', ' .join (summary_parts )} )"
21962252 )
2253+ if dismissed_count > 0 :
2254+ count_text += f" + { dismissed_count } disputed"
2255+ lines .append (count_text )
21972256 lines .append ("" )
21982257
21992258 lines .append ("---" )
0 commit comments