Skip to content

fix(llm): stabilize consensus finding severity - #152

Merged
vineethsai7 merged 2 commits into
mainfrom
codex/issue-125-consensus-severity
Aug 3, 2026
Merged

fix(llm): stabilize consensus finding severity#152
vineethsai7 merged 2 commits into
mainfrom
codex/issue-125-consensus-severity

Conversation

@vineethsai7

@vineethsai7 vineethsai7 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • make LLM consensus severity selection independent of response order
  • count at most one vote per finding key and run, using that run's highest severity
  • retain the highest severity observed for majority-agreed findings
  • expose agreement, severity-vote, successful-run, failed-run, and missing-vote metadata
  • document the remaining model-sampling limits of consensus mode

Validation

  • uv run ruff check .
  • uv run pytest -q tests/test_llm_analyzer.py (94 passed)
  • uv run pytest -q (1,597 passed, 5 skipped, 1 xfailed)
  • pre-commit hooks
  • CodeRabbit local review: 0 findings

Closes #125

Summary by CodeRabbit

  • New Features

    • Improved LLM consensus analysis with majority-based finding retention.
    • Findings now retain the highest severity observed across votes.
    • Failed or missing analysis runs are included in consensus thresholds.
    • Consensus results include clearer voting details and consistent ordering.
  • Documentation

    • Expanded guidance on consensus behavior, severity handling, failures, and model variability.

@vineethsai7
vineethsai7 marked this pull request as ready for review August 3, 2026 20:35
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@vineethsai7, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 3 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c6f45c3f-4849-4990-8968-eb185dadd032

📥 Commits

Reviewing files that changed from the base of the PR and between 65a25eb and 4ae1228.

📒 Files selected for processing (2)
  • README.md
  • docs/architecture/analyzers/llm-analyzer.md
📝 Walkthrough

Walkthrough

Changes

LLM consensus behavior

Layer / File(s) Summary
Consensus aggregation and severity resolution
skill_scanner/core/analyzers/llm_analyzer.py
Consensus deduplicates findings per run, retains the highest severity, tracks failed runs, uses the configured-run denominator, records metadata, and sorts results deterministically.
Consensus behavior validation
tests/test_llm_analyzer.py
Tests cover severity selection, run-order independence, failed and empty runs, denominator handling, and majority filtering.
Consensus contract documentation
README.md, docs/architecture/analyzers/llm-analyzer.md, docs/features/index.md
Documentation describes voting, severity retention, missing votes, metadata, and remaining nondeterminism.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ConfiguredLLMRuns
  participant ConsensusAggregation
  participant FindingsOutput
  ConfiguredLLMRuns->>ConsensusAggregation: provide findings and run results
  ConsensusAggregation->>ConsensusAggregation: deduplicate findings and rank severities
  ConsensusAggregation->>FindingsOutput: emit majority findings and consensus metadata
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The implementation selects the highest observed severity, normalizes per-run votes, tracks vote outcomes, and documents remaining nondeterminism for issue #125.
Out of Scope Changes check ✅ Passed The code, tests, and documentation changes directly support the consensus severity and nondeterminism objectives in issue #125.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: stabilizing LLM consensus finding severity.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/issue-125-consensus-severity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
skill_scanner/core/analyzers/llm_analyzer.py (1)

542-607: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consensus aggregation logic is correct.

The dedup-per-run, vote-tallying, and majority-filter logic correctly implements order-independent, highest-observed severity selection. This matches the tests in tests/test_llm_analyzer.py:559-627, including run-order permutations, failed-run denominator tracking, and below-threshold rejection.

Consider extracting the vote-tallying loop (lines 547-572) into a helper method. The line-range-change-details flags this block as high complexity, and splitting it improves readability without changing behavior.

♻️ Proposed refactor to extract vote tallying
+    `@staticmethod`
+    def _tally_consensus_votes(
+        all_run_findings: list[list[Finding]],
+    ) -> tuple[
+        dict[tuple[str, str, str], int],
+        dict[tuple[str, str, str], Finding],
+        dict[tuple[str, str, str], dict[Severity, int]],
+    ]:
+        """Count one vote per run per finding key, keeping the highest severity seen."""
+        finding_counts: dict[tuple[str, str, str], int] = {}
+        finding_map: dict[tuple[str, str, str], Finding] = {}
+        severity_votes: dict[tuple[str, str, str], dict[Severity, int]] = {}
+
+        for run_findings in all_run_findings:
+            findings_by_key: dict[tuple[str, str, str], Finding] = {}
+            for f in run_findings:
+                key = (f.rule_id, f.category.value, f.file_path or "")
+                previous = findings_by_key.get(key)
+                if (
+                    previous is None
+                    or _CONSENSUS_SEVERITY_RANK[f.severity] > _CONSENSUS_SEVERITY_RANK[previous.severity]
+                ):
+                    findings_by_key[key] = f
+
+            for key, finding in findings_by_key.items():
+                finding_counts[key] = finding_counts.get(key, 0) + 1
+                votes_for_key = severity_votes.setdefault(key, {})
+                votes_for_key[finding.severity] = votes_for_key.get(finding.severity, 0) + 1
+
+                current = finding_map.get(key)
+                if (
+                    current is None
+                    or _CONSENSUS_SEVERITY_RANK[finding.severity] > _CONSENSUS_SEVERITY_RANK[current.severity]
+                ):
+                    finding_map[key] = finding
+
+        return finding_counts, finding_map, severity_votes
+
     async def _consensus_analyze(self, messages: list[dict], skill: Skill) -> list[Finding]:
         ...
-        # Count one vote per run for each unique finding. If a single response
-        # duplicates a key at different severities, that run casts its highest
-        # severity only.
-        finding_counts: dict[tuple[str, str, str], int] = {}
-        finding_map: dict[tuple[str, str, str], Finding] = {}
-        severity_votes: dict[tuple[str, str, str], dict[Severity, int]] = {}
-
-        for run_findings in all_run_findings:
-            findings_by_key: dict[tuple[str, str, str], Finding] = {}
-            for f in run_findings:
-                key = (f.rule_id, f.category.value, f.file_path or "")
-                previous = findings_by_key.get(key)
-                if (
-                    previous is None
-                    or _CONSENSUS_SEVERITY_RANK[f.severity] > _CONSENSUS_SEVERITY_RANK[previous.severity]
-                ):
-                    findings_by_key[key] = f
-
-            for key, finding in findings_by_key.items():
-                finding_counts[key] = finding_counts.get(key, 0) + 1
-                votes_for_key = severity_votes.setdefault(key, {})
-                votes_for_key[finding.severity] = votes_for_key.get(finding.severity, 0) + 1
-
-                current = finding_map.get(key)
-                if (
-                    current is None
-                    or _CONSENSUS_SEVERITY_RANK[finding.severity] > _CONSENSUS_SEVERITY_RANK[current.severity]
-                ):
-                    finding_map[key] = finding
+        # Count one vote per run for each unique finding. If a single response
+        # duplicates a key at different severities, that run casts its highest
+        # severity only.
+        finding_counts, finding_map, severity_votes = self._tally_consensus_votes(all_run_findings)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skill_scanner/core/analyzers/llm_analyzer.py` around lines 542 - 607, Extract
the per-run deduplication and vote-tallying block in the consensus aggregation
flow into a focused helper method, preserving the existing
highest-severity-per-key selection and updates to finding_counts, finding_map,
and severity_votes. Update the surrounding method to invoke the helper without
changing majority filtering, ordering, metadata, or failure-denominator
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/architecture/analyzers/llm-analyzer.md`:
- Around line 218-221: Restrict the documentation claims to deterministic
severity selection rather than overall aggregation stability. Update
docs/architecture/analyzers/llm-analyzer.md lines 218-221 and README.md lines
196-203 to use matching severity-scoped wording, while preserving the
clarification that complete equal-severity findings may still vary by response
order.

---

Nitpick comments:
In `@skill_scanner/core/analyzers/llm_analyzer.py`:
- Around line 542-607: Extract the per-run deduplication and vote-tallying block
in the consensus aggregation flow into a focused helper method, preserving the
existing highest-severity-per-key selection and updates to finding_counts,
finding_map, and severity_votes. Update the surrounding method to invoke the
helper without changing majority filtering, ordering, metadata, or
failure-denominator behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 197fac20-dd5c-44dc-95fa-f7d7d8866de2

📥 Commits

Reviewing files that changed from the base of the PR and between 5dca0c5 and 65a25eb.

📒 Files selected for processing (5)
  • README.md
  • docs/architecture/analyzers/llm-analyzer.md
  • docs/features/index.md
  • skill_scanner/core/analyzers/llm_analyzer.py
  • tests/test_llm_analyzer.py

Comment thread docs/architecture/analyzers/llm-analyzer.md Outdated
@vineethsai7
vineethsai7 merged commit 88b95c4 into main Aug 3, 2026
12 checks passed
@vineethsai7
vineethsai7 deleted the codex/issue-125-consensus-severity branch August 3, 2026 20:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Meta-analyzer: CRITICAL finding disappears on some runs of the same skill (Haiku 3.5, 5/5 reproducible)

1 participant