Skip to content

feat(azure): publish review findings inline - #2651

Merged
IsmaelMartinez merged 1 commit into
The-PR-Agent:mainfrom
TLA020:feature/inline-key-issues
Aug 18, 2026
Merged

feat(azure): publish review findings inline#2651
IsmaelMartinez merged 1 commit into
The-PR-Agent:mainfrom
TLA020:feature/inline-key-issues

Conversation

@TLA020

@TLA020 TLA020 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds opt-in Azure DevOps support for publishing /review findings as inline threads beside the affected code.

Scope

Azure DevOps only. The [pr_reviewer] inline_key_issues setting is off by default.

When enabled, the reviewer:

  • validates each file and right-side line range before publishing;
  • converts any suggestion fence to plain code;
  • confirms current-run posts from successful Azure thread creation without a second thread listing;
  • clears cached publication state when the provider changes pull requests;
  • keeps failed or unanchorable findings in the summary;
  • continues after per-finding failures;
  • publishes identical findings at each distinct current-run range while collapsing exact duplicates;
  • deduplicates by canonical path and full body across runs by reading Azure's complete PR thread list once per review run;
  • preserves the caller data.

GitHub and GitLab behavior is unchanged.

Validation

  • 100 focused unit tests pass for reviewer, Azure provider, deduplication, and Markdown behavior.
  • 1,783 unit tests pass, with 1 skipped and 1 expected failure.
  • Ruff passes on the added helper and test coverage.
  • git diff --check passes.

Closes #2650.

@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

feat(review): publish key issues as inline comments

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Add opt-in setting to post each key issue as a line-anchored review comment.
• Keep unanchorable or failed-to-publish findings in the summary to avoid loss.
• Deduplicate inline findings across reruns using existing fingerprint markers.
Diagram

graph TD
  A["/review (PRReviewer)"] --> B{"inline_key_issues enabled?"} --> C["Build anchored comment"] --> D["Dedup store (fingerprints)"] --> E["Git provider publish" ]
  B --> F["Render summary comment"]
  E --> G{"Publish succeeded?"} --> H["Remove from summary"]
  G --> I["Keep in summary"]

  subgraph Legend
    direction LR
    _proc["Process"] ~~~ _dec{"Decision"}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Always keep key issues in the summary (also post inline)
  • ➕ Guarantees a single consolidated view even when inline comments are posted
  • ➕ Reduces chance reviewers miss an inline comment in large diffs
  • ➖ Duplicates content and increases noise (summary + inline threads)
  • ➖ Harder to tell which issues are already addressed inline
2. Post one inline comment per file (aggregate issues per file)
  • ➕ Fewer total comments on large PRs
  • ➕ Keeps feedback near code without flooding conversations
  • ➖ Less precise anchoring when multiple issues map to different ranges
  • ➖ Still needs per-issue validation and fallback rules
3. Use suggestion blocks for key issues when a patch is possible
  • ➕ Enables one-click apply for mechanical fixes
  • ➕ Aligns with existing suggestion publishing paths
  • ➖ Key issues are often non-edit observations; suggestion blocks can generate broken patches
  • ➖ Adds complexity deciding when a finding is safely committable

Recommendation: Proceed with the PR’s approach: opt-in inline publication with strict anchoring validation, per-issue failure isolation, and removing an issue from the summary only after successful publish. This balances reviewer ergonomics (feedback at the line) with safety (no lost findings) and avoids misuse of suggestion blocks for prose-only observations.

Files changed (4) +238 / -1

Enhancement (1) +109 / -1
pr_reviewer.pyPublish key issues as safe inline comments with dedup and fallback +109/-1

Publish key issues as safe inline comments with dedup and fallback

• Adds an opt-in path that converts each key issue into a line-anchored markdown comment and publishes it via the git provider. Validates file presence and right-side line ranges, deduplicates across reruns using fingerprint markers, and keeps any unpublishable findings in the summary by rendering from a deep-copied data structure.

pr_agent/tools/pr_reviewer.py

Tests (1) +124 / -0
test_pr_reviewer_core.pyAdd unit tests for inline key issue publishing behavior +124/-0

Add unit tests for inline key issue publishing behavior

• Adds coverage for default-off behavior, enabled inline publishing, no suggestion fences in bodies, anchoring validation fallbacks, per-issue error isolation, and cross-run dedup preventing duplicate inline comments.

tests/unittest/test_pr_reviewer_core.py

Documentation (1) +4 / -0
review.mdDocument new inline_key_issues reviewer option +4/-0

Document new inline_key_issues reviewer option

• Adds documentation for the new pr_reviewer.inline_key_issues flag and explains that unanchorable findings remain in the summary. Clarifies default behavior (false).

docs/docs/tools/review.md

Other (1) +1 / -0
configuration.tomlExpose inline_key_issues setting in default configuration +1/-0

Expose inline_key_issues setting in default configuration

• Introduces inline_key_issues=false under [pr_reviewer] with an inline description of the behavior and fallback to summary.

pr_agent/settings/configuration.toml

@qodo-code-review

qodo-code-review Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (2) 📜 Skill insights (0)

Grey Divider


Action required

1. Same-run duplicate finding silently dropped ✓ Resolved 🐞 Bug ≡ Correctness
Description
Distinct findings can collide on the same-run candidate_fingerprints check because the fingerprint
is a lossy truncation, and when a later key issue matches an earlier fingerprint the code
continues without appending it to remaining_issues, causing the finding to vanish from both
published inline comments and the review summary. This is the only rejection branch in the loop that
drops an issue entirely, contradicting the guarantee that “a finding is never lost.”
Code

pr_agent/tools/pr_reviewer.py[R410-415]

+                if fingerprint in candidate_fingerprints:
+                    continue
+                comment["body"] = body_with_markers(comment["body"], fingerprint, None,
+                                                     getattr(self.git_provider, "max_comment_chars", None))
+                candidate_fingerprints.add(fingerprint)
+                candidates.append((issue, fingerprint, comment))
Relevance

●●● Strong

Silent dropping contradicts stated “never lost” guarantee; simple fix to keep issue in remaining
list is likely welcomed.

PR-#2424

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
In _publish_key_issues_as_inline_comments, the candidate_fingerprints collision path at
pr_agent/tools/pr_reviewer.py[410-411] executes a continue without calling
remaining_issues.append(issue), unlike the comment is None branch at [402-404] and the exception
handling at [416-418], both of which explicitly append the issue back to remaining_issues. Because
body_fingerprint truncates normalized content to 80 characters and omits the end line, distinct
findings (e.g., same file and start line but differing after character 80 or differing in range) can
produce the same compact fingerprint, triggering this silent skip and thus removing the later
finding from both the inline-comment output and the rendered summary; the existing test
test_duplicate_key_issue_is_published_once only asserts that a single comment is published and
that key_issues_to_review is absent, but does not assert the skipped duplicate is retained,
leaving this data-loss path untested.

pr_agent/tools/pr_reviewer.py[395-418]
tests/unittest/test_pr_reviewer_core.py[327-334]
pr_agent/algo/inline_comment_dedup.py[61-66]
pr_agent/tools/pr_reviewer.py[405-415]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In `_publish_key_issues_as_inline_comments`, when a key issue’s fingerprint matches another candidate queued earlier in the same run, the code executes `continue` without adding the issue to `remaining_issues`. Because the fingerprint is lossy (normalized body truncated to 80 characters and not including the full range), distinct findings can collide and the later finding is silently removed from both the inline-comment output and the review summary.

## Issue Context
The loop in `_publish_key_issues_as_inline_comments` maintains `candidates` (to publish) and `remaining_issues` (to keep in the summary). Every other rejection path—when a comment can’t be built (`comment is None`) or when an exception occurs while building/publishing—appends the issue to `remaining_issues`, but the same-run duplicate-fingerprint branch does not, making it the only path that can drop findings entirely.

Additionally, the `body_fingerprint` logic truncates normalized content to 80 characters and omits the end line, so collisions are possible for distinct issues (e.g., same file and start line but different text after character 80 and/or different ranges). Either the identity used for same-run deduplication should be collision-resistant (include complete body and full range), or the implementation should ensure that when only the compact fingerprint matches but the full identity differs, the issue is retained in the summary. Add test coverage that creates two same-file, same-start findings that diverge after character 80 and verifies the later one is not lost (even if it is not published as an inline comment).

## Fix Focus Areas
- pr_agent/tools/pr_reviewer.py[402-418]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Unsupported providers never verify ✓ Resolved 🐞 Bug ☼ Reliability
Description
InlineCommentStore can enumerate existing inline comments only for GithubProvider and
GitLabProvider; on other providers it raises NotImplementedError which InlineCommentStore.load()
swallows, causing _is_inline_key_issue_published() to always return False and making the same inline
findings eligible to be re-posted on every /review run.
Code

pr_agent/tools/pr_reviewer.py[R410-411]

+                if (self.git_provider.publish_code_suggestions([comment]) and
+                        self._is_inline_key_issue_published(fingerprint)):
Evidence
The dedup/verification layer only supports GitHub/GitLab; for other providers it raises
NotImplementedError and the store degrades to an empty seen-set, so verification cannot succeed and
cross-run dedup cannot work.

pr_agent/tools/pr_reviewer.py[376-424]
pr_agent/algo/inline_comment_dedup.py[108-133]
pr_agent/algo/inline_comment_dedup.py[150-165]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
For providers other than GitHub/GitLab, inline-comment enumeration is unimplemented; verification always fails and the dedup store cannot ever detect previously-posted markers.

### Issue Context
- `iter_existing_inline_comment_bodies()` raises `NotImplementedError` for unsupported provider class names.
- `InlineCommentStore.load()` catches exceptions and proceeds with an empty seen-set.
- The reviewer removes findings from the summary only after verification, and updates the dedup store only on verification success.

### Fix Focus Areas
- pr_agent/tools/pr_reviewer.py[376-424]
- pr_agent/algo/inline_comment_dedup.py[108-133]
- pr_agent/algo/inline_comment_dedup.py[150-170]

### Suggested fix
- Detect whether inline comment enumeration is supported (e.g., via `git_provider.is_supported(...)` or by checking provider type) before enabling inline publishing.
- If unsupported, skip inline publishing entirely (leave findings in the summary) and log a clear warning stating the provider is unsupported for inline-key-issues verification/dedup.
- Alternatively, implement `iter_existing_inline_comment_bodies()` for the additional provider(s) you intend to support.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Fingerprint lacks line anchor ✓ Resolved 🐞 Bug ≡ Correctness
Description
_publish_key_issues_as_inline_comments computes the dedup fingerprint with target_line_no=None, so
two different findings in the same file with the same normalized body can collide and be treated as
already posted. This can skip publishing and also remove a distinct finding from the review summary.
Code

pr_agent/tools/pr_reviewer.py[R398-401]

+                fingerprint = body_fingerprint(comment['relevant_file'], None, comment['body'])
+                if store.seen(fingerprint):
+                    published += 1  # already on the PR, so it does not belong in the summary either
+                    continue
Evidence
The new inline publishing code fingerprints key issues without any line anchor. The dedup module
documents and implements the fingerprint key as including an “anchor line”, so dropping it increases
collisions between distinct findings in the same file.

pr_agent/tools/pr_reviewer.py[386-405]
pr_agent/algo/inline_comment_dedup.py[12-20]
pr_agent/algo/inline_comment_dedup.py[61-66]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Inline key-issue comments are deduplicated using `body_fingerprint(relevant_file, target_line_no, body)`, but the new code passes `target_line_no=None`. This makes the fingerprint ignore the finding’s location, causing collisions between distinct findings in the same file and potentially dropping findings from both inline publishing and the summary.

### Issue Context
- Each key issue already has `start_line`/`end_line`.
- `inline_comment_dedup.body_fingerprint` is explicitly designed to incorporate an anchor line into the hash key.

### Fix Focus Areas
- pr_agent/tools/pr_reviewer.py[386-406]
- pr_agent/algo/inline_comment_dedup.py[61-66]

### Suggested fix
- Compute the fingerprint using a stable location anchor, e.g. `start_line` (or a `f"{start_line}-{end_line}"` string), instead of `None`.
- Add a unit test covering two key issues in the same file with identical bodies but different line ranges to ensure both publish (and neither is dropped from summary).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View high (1)
4. Publish success not reliable ✓ Resolved 🐞 Bug ☼ Reliability
Description
The new inline-key-issues path assumes publish_code_suggestions() returns False when a comment was
not published and removes the finding from the summary on True. Several providers return True even
when individual suggestions fail/are skipped (or when they publish to non-inline outputs), so
findings can disappear from the summary without ever becoming an inline comment.
Code

pr_agent/tools/pr_reviewer.py[R404-406]

+                if self.git_provider.publish_code_suggestions([comment]):
+                    store.add(fingerprint)
+                    published += 1
Evidence
The reviewer removes findings from the summary when publish_code_suggestions returns True, but
multiple providers return True even after per-suggestion failures/skips or for non-inline outputs.
This breaks the PR’s safety guarantee that findings only leave the summary once posted.

pr_agent/tools/pr_reviewer.py[402-413]
pr_agent/git_providers/azuredevops_provider.py[87-125]
pr_agent/git_providers/gitlab_provider.py[728-768]
pr_agent/git_providers/local_git_provider.py[139-164]
pr_agent/git_providers/plain_diff_provider.py[173-190]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`_publish_key_issues_as_inline_comments` treats the boolean return from `git_provider.publish_code_suggestions([comment])` as an authoritative “this specific comment was posted” signal. However, some providers return `True` unconditionally (even after catching per-suggestion failures or skipping), and local/plain-diff providers implement this method by writing to files/stdout.

Result: a key issue can be removed from the summary and marked “published” even when no inline comment exists.

### Issue Context
Provider implementations vary:
- Azure DevOps catches exceptions and always returns `True`.
- GitLab catches exceptions and always returns `True`.
- Local/plain-diff providers write output and return `True` (not an inline publish).

### Fix Focus Areas
- pr_agent/tools/pr_reviewer.py[402-417]
- pr_agent/git_providers/azuredevops_provider.py[87-125]
- pr_agent/git_providers/gitlab_provider.py[728-768]
- pr_agent/git_providers/local_git_provider.py[139-164]
- pr_agent/git_providers/plain_diff_provider.py[173-190]

### Suggested fix
Implement a “only remove from summary when safely posted” check that does not depend on unreliable bool returns, for example:
- Gate inline-key-issues publishing to providers that actually support inline comments (e.g., via `is_supported(...)`), and for others always keep findings in the summary.
- Or change the provider contract to return a per-suggestion result (or raise on failure), and update providers that currently return `True` unconditionally.
- Avoid counting a finding as published unless the provider reports that specific comment/thread was created.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. Inline threads posted before publish gating check 🐞 Bug ☼ Reliability ⭐ New
Description
_prepare_pr_review() calls _publish_key_issues_as_inline_comments(), which can create real Azure
DevOps inline threads, before run() evaluates
should_publish/_should_publish_review_no_suggestions(). If the overall review ends up suppressed
(e.g. publish_output_no_suggestions=False and the summary later reads as having no remaining issues
due to unrelated pre-existing conditions, or publish_output flips mid-run), inline comments can
already be live on the PR while the summary comment path diverges from what was intended, so users
can end up with inline threads that are not accompanied by a corresponding published summary,
breaking the assumption that the two happen atomically.
Code

pr_agent/tools/pr_reviewer.py[R288-290]

+        if get_settings().config.publish_output and get_settings().pr_reviewer.get('inline_key_issues', False):
+            data = self._publish_key_issues_as_inline_comments(data)
+
Relevance

●●● Strong

Accepted reliability precedents address irreversible side effects and PR reviewer control-flow
ordering issues.

PR-#2381
PR-#2404

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
run() computes pr_review = self._prepare_pr_review() (line 185) then only afterward computes
`should_publish = get_settings().config.publish_output and
self._should_publish_review_no_suggestions(pr_review)` (line 188) and can return without publishing
the summary (lines 189-195). But _prepare_pr_review() already triggers
self.git_provider.publish_code_suggestions(...) as a side effect via
_publish_key_issues_as_inline_comments at line 288-290, which is not gated by
_should_publish_review_no_suggestions. This creates an ordering where irreversible external side
effects (thread creation) occur before the tool decides whether the overall review output should be
published at all.

pr_agent/tools/pr_reviewer.py[185-195]
pr_agent/tools/pr_reviewer.py[288-290]
pr_agent/tools/pr_reviewer.py[443-445]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`_prepare_pr_review()` calls `_publish_key_issues_as_inline_comments()`, which publishes real inline Azure DevOps threads, before `run()` decides via `_should_publish_review_no_suggestions()` whether the overall review summary will actually be published. This can create a state where inline comments exist on the PR but the summary review comment is withheld.

## Issue Context
`run()` in `pr_agent/tools/pr_reviewer.py` calls `_prepare_pr_review()` (which may publish inline comments as a side effect) and only afterward checks `should_publish`. If `should_publish` ends up false, the function returns without publishing the summary, but inline comments already went out.

## Fix Focus Areas
- pr_agent/tools/pr_reviewer.py[185-195]
- pr_agent/tools/pr_reviewer.py[288-290]

Consider deferring the inline-publish call until after the should_publish decision is made, or making the inline-publish decision consistent with (dependent on) the eventual overall publish decision.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Review documentation line too long 📘 Rule violation ⚙ Maintainability ⭐ New
Description
The new inline_key_issues documentation is 269 characters on one physical line, exceeding the
120-character limit for modified source files.
Code

docs/docs/tools/review.md[83]

+        <td>Azure DevOps only. If set to true, each key issue is published as an inline thread. A finding leaves the review summary when a matching thread exists or Azure accepts the new thread. Findings that cannot be anchored or published stay in the summary. Default is false.</td>
Relevance

●●● Strong

Repository history consistently accepts modified-source line-length fixes, including documentation
wrapping precedents.

PR-#2470
PR-#2212

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2694690 limits modified source lines to 120 characters, while the added
documentation at line 83 is 269 characters long.

Rule 2694690: Enforce maximum line length of 120 characters
docs/docs/tools/review.md[83-83]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `inline_key_issues` documentation line exceeds the 120-character maximum.

## Issue Context
Wrap the HTML text without changing its rendered meaning.

## Fix Focus Areas
- docs/docs/tools/review.md[83-83]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. _publish_key_issues_as_inline_comments mixes responsibilities 📘 Rule violation ⚙ Maintainability ⭐ New
Description
The method loads and transforms diff data, coordinates Azure publication and verification, and
rewrites review output. This combines external I/O, workflow coordination, and data transformation
in one function.
Code

pr_agent/tools/pr_reviewer.py[R390-393]

+    def _publish_key_issues_as_inline_comments(self, data: dict) -> dict:
+        issues = (data.get("review") or {}).get("key_issues_to_review")
+        if not isinstance(issues, list) or not issues:
+            return data
Relevance

●● Moderate

Responsibility-splitting feedback is plausible, but historical evidence does not directly establish
this exact refactor as required.

PR-#2598

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2694714 treats transformation plus external-system coordination as distinct
responsibilities. The method begins at line 390, builds publication candidates at lines 399-437,
performs provider I/O and verification at lines 443-467, and rewrites the review data at lines
469-478.

Rule 2694714: Limit functions to a single, clearly defined responsibility
pr_agent/tools/pr_reviewer.py[390-437]
pr_agent/tools/pr_reviewer.py[443-478]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`_publish_key_issues_as_inline_comments` combines comment preparation, provider I/O, verification, and review-result transformation.

## Issue Context
Keep the method as a small coordinator and extract focused helpers for candidate preparation, batch publication/verification, and summary updates.

## Fix Focus Areas
- pr_agent/tools/pr_reviewer.py[390-478]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (15)
8. Raw-body key-issue fingerprint lacks normalization 🐞 Bug ☼ Reliability ⭐ New
Description
key_issue_fingerprint hashes the full, unnormalized issue body (header + issue_content) with no
whitespace/case normalization or truncation, unlike the pre-existing body_fingerprint which strips
tags/leads, collapses whitespace, truncates to 80 chars, and lowercases before hashing. Because
LLM-generated review text is non-deterministic across runs, even a minor rewording of the same
underlying finding produces a completely different SHA-256 hash, so store.seen(fingerprint) fails to
recognize the same logical issue and it gets republished as a new inline Azure DevOps thread on a
later run, defeating the PR's stated cross-run deduplication goal.
Code

pr_agent/algo/inline_comment_dedup.py[R71-73]

+def key_issue_fingerprint(relevant_file: str, body: str) -> str:
+    key = f"{relevant_file}|{body}"
+    return hashlib.sha256(key.encode("utf-8")).hexdigest()[:12]
Relevance

●● Moderate

Normalization and marker-invariance bugs are accepted, but this PR explicitly tests full-body
differentiation; intent conflicts.

PR-#2424

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
key_issue_fingerprint hashes f"{relevant_file}|{body}" verbatim with no normalization, unlike
body_fingerprint which normalizes via _LEAD_RE, _TAG_RE, _WS_RE and truncates to 80 chars before
hashing (lines 63-68). This fingerprint is used for cross-run dedup at pr_reviewer.py:423-425, so
any wording drift in the LLM's re-description of the same finding across /review runs breaks dedup.

pr_agent/algo/inline_comment_dedup.py[63-68]
pr_agent/algo/inline_comment_dedup.py[71-73]
pr_agent/tools/pr_reviewer.py[423-425]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`key_issue_fingerprint` hashes the raw, full issue body with no normalization, making cross-run deduplication fragile against minor LLM wording changes between runs, unlike the existing `body_fingerprint` helper in the same module which normalizes and truncates before hashing.

## Issue Context
`pr_agent/algo/inline_comment_dedup.py` already has a normalization pattern (`_LEAD_RE`, `_TAG_RE`, `_WS_RE`, truncate-to-80-lowercase) used by `body_fingerprint`. The new `key_issue_fingerprint` (added by this PR) does not reuse this normalization.

## Fix Focus Areas
- pr_agent/algo/inline_comment_dedup.py[63-73]

Apply similar normalization (whitespace collapsing, case folding, and/or truncation) to the body before hashing in `key_issue_fingerprint`, or reuse `body_fingerprint`'s normalization logic, so that minor rewording of the same underlying finding across runs still produces a matching fingerprint.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Dedup import exceeds limit ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The added deduplication import is 130 characters long, exceeding the 120-character maximum for
Python source.
Code

tests/unittest/test_pr_reviewer_core.py[6]

+from pr_agent.algo.inline_comment_dedup import body_with_markers, get_inline_comment_store, key_issue_fingerprint
Relevance

●●● Strong

A changed Python import exceeding the explicit 120-character rule is a trivial deterministic
formatting fix.

PR-#2424
PR-#2584

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The cited rules require changed Python and source lines to remain at or below 120 characters; the
added physical import line is 130 characters long.

Rule 2694655: Enforce 120-character maximum line length in Python source per Ruff config
Rule 2694690: Enforce maximum line length of 120 characters
tests/unittest/test_pr_reviewer_core.py[6-6]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new import exceeds the 120-character line-length limit.

## Issue Context
Convert the import to a parenthesized multiline import while preserving imported names.

## Fix Focus Areas
- tests/unittest/test_pr_reviewer_core.py[6-6]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Publication warning exceeds limit ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The added warning at line 450 is 123 characters long, exceeding the 120-character maximum for Python
source.
Code

pr_agent/tools/pr_reviewer.py[450]

+                get_logger().warning(f"Failed to publish review findings as Azure DevOps threads, error: {e}",
Relevance

●●● Strong

Directly matches accepted repository line-length enforcement for newly added logging statements.

PR-#2424
PR-#2584

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The cited rules require changed Python and source lines to remain at or below 120 characters; the
added physical line is 123 characters long.

Rule 2694655: Enforce 120-character maximum line length in Python source per Ruff config
Rule 2694690: Enforce maximum line length of 120 characters
pr_agent/tools/pr_reviewer.py[450-450]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new publication warning exceeds the 120-character line-length limit.

## Issue Context
Wrap the call without changing its message or behavior.

## Fix Focus Areas
- pr_agent/tools/pr_reviewer.py[450-451]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. Verification warning exceeds limit ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The added warning at line 384 is 134 characters long, exceeding the 120-character maximum for Python
source.
Code

pr_agent/tools/pr_reviewer.py[384]

+            get_logger().warning(f"Inline key-issue publishing cannot verify new Azure DevOps threads, error: {e}; "
Relevance

●●● Strong

Directly matches accepted repository line-length enforcement, including long PRReviewer conditions
and warning strings.

PR-#2381
PR-#2584

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The cited rules require changed Python and source lines to remain at or below 120 characters; the
added physical line is 134 characters long.

Rule 2694655: Enforce 120-character maximum line length in Python source per Ruff config
Rule 2694690: Enforce maximum line length of 120 characters
pr_agent/tools/pr_reviewer.py[384-384]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new warning statement exceeds the 120-character line-length limit.

## Issue Context
Wrap the call without changing its message or behavior.

## Fix Focus Areas
- pr_agent/tools/pr_reviewer.py[384-385]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


12. Azure inline cache leaks ✓ Resolved 🐞 Bug ≡ Correctness
Description
AzureDevopsProvider stores recently published inline comment bodies on the provider instance and
reuses them for verification, but the cache is never cleared when set_pr() switches the provider
to a different PR. If a provider instance is reused across PRs and a finding has the same (file,
body) fingerprint as a cached body from a prior PR, PRReviewer can incorrectly treat the finding as
already published and remove it from the review summary without creating a thread on the current PR.
Code

pr_agent/git_providers/azuredevops_provider.py[R127-130]

+                if recent_bodies is None:
+                    recent_bodies = []
+                    self._published_inline_comment_bodies = recent_bodies
+                recent_bodies.append(body)
Relevance

●●● Strong

Accepted precedent targets stale PR-scoped provider state; clearing inline cache on set_pr is a
direct deterministic fix.

PR-#2381
PR-#2492

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new code appends bodies to a provider-instance cache on successful publish, and
get_inline_comment_bodies() always includes that cache. The provider’s set_pr() method changes
PR identity but does not reset this cache, and there is explicit in-file indication the provider
instance can be reused. PRReviewer’s inline-key-issues path uses the loaded store to decide whether
a finding is already published and to remove findings from the summary, so stale cached bodies can
suppress findings on a different PR if fingerprints collide.

pr_agent/git_providers/azuredevops_provider.py[91-131]
pr_agent/git_providers/azuredevops_provider.py[764-785]
pr_agent/git_providers/azuredevops_provider.py[192-207]
pr_agent/tools/pr_reviewer.py[392-418]
pr_agent/tools/pr_reviewer.py[459-464]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`AzureDevopsProvider` caches successfully posted inline-comment bodies in `_published_inline_comment_bodies` and `get_inline_comment_bodies()` always seeds its results from that cache. Because `set_pr()` does not clear or scope this cache, a reused provider instance can carry cached bodies from a different PR, causing false “already published” matches during inline-key-issue dedup/verification.

## Issue Context
This PR introduces the cache append in `publish_code_suggestions()` and reads it back in `get_inline_comment_bodies()`.
PRReviewer uses `InlineCommentStore.load()` and `store.seen(fingerprint)` to decide whether to skip publishing and/or remove `key_issues_to_review` from the summary, so stale cache entries can suppress findings.

## Fix Focus Areas
- pr_agent/git_providers/azuredevops_provider.py[192-196]
- pr_agent/git_providers/azuredevops_provider.py[125-131]
- pr_agent/git_providers/azuredevops_provider.py[764-784]

### Suggested fix approach
- Clear `_published_inline_comment_bodies` in `set_pr()` (or whenever `pr_num/repo_slug/workspace_slug` changes), so the cache is per-PR.
- Alternatively, store cached bodies in a dict keyed by `(workspace_slug, repo_slug, pr_num)` and only use the current key in `get_inline_comment_bodies()`.
- (Optional) deduplicate cached bodies (e.g., maintain a set alongside the list) to avoid unbounded growth in long-lived processes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


13. GitLab drops range anchoring ✓ Resolved 🐞 Bug ≡ Correctness
Description
GitLab is enabled for verified inline publication, but its publication path anchors multi-line key
issues only at relevant_lines_start and never sends the requested end line. Fingerprint
verification then removes the finding from the summary even though its full range was not preserved.
Code

pr_agent/algo/inline_comment_dedup.py[R146-147]

+def can_verify_inline_comment_publication(git_provider) -> bool:
+    return type(git_provider).__name__ in {"GithubProvider", "GitLabProvider"}
Relevance

●●● Strong

Team recently tightened inline-comment verification/dedup correctness; GitLab range loss would
violate “never lose findings” goal.

PR-#2424

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new capability gate explicitly opts GitLab into verified publication. PRReviewer passes both
range endpoints, but GitLab's publish_code_suggestions derives its position from
relevant_lines_start and send_inline_comment creates only one new_line; verification only
scans for the embedded fingerprint, so it cannot detect the lost end line.

pr_agent/algo/inline_comment_dedup.py[146-147]
pr_agent/tools/pr_reviewer.py[362-366]
pr_agent/git_providers/gitlab_provider.py[730-765]
pr_agent/git_providers/gitlab_provider.py[627-645]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
GitLab is considered capable of verified inline key-issue publication, but multi-line findings are posted as single-line comments at the start line. Preserve the requested range before treating the finding as successfully published.

## Issue Context
Key-issue comments contain both `relevant_lines_start` and `relevant_lines_end`, while the current GitLab path constructs a position from the start line only. Fingerprint presence verifies the body was posted, not that its requested range was retained.

## Fix Focus Areas
- pr_agent/algo/inline_comment_dedup.py[146-147]
- pr_agent/git_providers/gitlab_provider.py[730-765]
- pr_agent/git_providers/gitlab_provider.py[602-645]
- tests/unittest/test_pr_reviewer_core.py[216-228]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


14. get_threads unbounded on every inline review 🐞 Bug ➹ Performance
Description
When inline_key_issues is enabled, every /review run now calls Azure DevOps get_threads() without
pagination or limits via get_inline_comment_bodies(), fetching all PR threads/comments; on
long-lived PRs with many historical comments this adds latency, timeout, and rate-limit risk on
every run, and if the API silently caps results, dedup can miss existing comments and republish
findings.
Code

pr_agent/git_providers/azuredevops_provider.py[R769-774]

+    def get_inline_comment_bodies(self) -> list[str]:
+        threads = self.azure_devops_client.get_threads(
+            repository_id=self.repo_slug,
+            pull_request_id=self.pr_num,
+            project=self.workspace_slug,
+        )
Relevance

●● Moderate

Azure performance fixes are accepted, but this API-listing design is intentional for cross-run dedup
and lacks close pagination precedent.

PR-#2381

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
get_inline_comment_bodies calls self.azure_devops_client.get_threads(...) with no continuation
token/paging, and is invoked from store.load() on every eligible /review run via
iter_existing_inline_comment_bodies, a call path that did not previously exist for /review.

pr_agent/git_providers/azuredevops_provider.py[769-789]
pr_agent/algo/inline_comment_dedup.py[153-154]
pr_agent/tools/pr_reviewer.py[405-410]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
get_inline_comment_bodies() in pr_agent/git_providers/azuredevops_provider.py issues an unbounded get_threads() call on every /review run when inline_key_issues is enabled, with no pagination handling.

## Issue Context
This is invoked once per /review run via InlineCommentStore.load() -> iter_existing_inline_comment_bodies(), a new recurring cost that scales with total PR comment history.

## Fix Focus Areas
- pr_agent/git_providers/azuredevops_provider.py[769-789]
- pr_agent/tools/pr_reviewer.py[405-410]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


15. Post-publication verification re-reads may miss new comments ✓ Resolved 🐞 Bug ☼ Reliability
Description
_published_inline_key_issue_fingerprints() builds a brand-new InlineCommentStore and re-lists all
PR/MR comments right after publish_code_suggestions() returns, instead of trusting the publish call
or reusing already-known state. If the provider's comment listing does not yet reflect the
just-created comment(s) (e.g. read-after-write lag or an API that lists comments differently than
discussions), a successfully posted finding is treated as unverified and gets kept in the review
summary in addition to the now-live inline comment, duplicating the finding for the user.
Code

pr_agent/tools/pr_reviewer.py[R371-376]

+    def _published_inline_key_issue_fingerprints(self, candidates: list[tuple[dict, str, dict]]) -> Optional[set[str]]:
+        verification_store = InlineCommentStore(self.git_provider)
+        verification_store.load()
+        if verification_store.load_failed:
+            return None
+        return {fingerprint for _, fingerprint, _ in candidates if verification_store.seen(fingerprint)}
Relevance

●● Moderate

Reliability concern is plausible, but involves provider consistency/lag tradeoffs; similar
anti-duplication hardening was accepted before.

PR-#2404

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
pr_agent/tools/pr_reviewer.py:371-376 constructs InlineCommentStore(self.git_provider) fresh and
calls .load(), which triggers iter_existing_inline_comment_bodies() -> a live
self.pr.get_comments() (GitHub) or
self.mr.discussions.list(get_all=True)/self.mr.notes.list(get_all=True) (GitLab) call, per
pr_agent/algo/inline_comment_dedup.py:118-143. This is a second, independent read of the provider
state performed immediately after publish, and its success is required for verified_fingerprints
to be non-None and to contain the new fingerprint (pr_reviewer.py:427-440); any staleness in that
read causes the finding to be duplicated (posted inline AND kept in the summary) rather than simply
staying in the summary as intended by the design.

pr_agent/tools/pr_reviewer.py[371-376]
pr_agent/algo/inline_comment_dedup.py[118-143]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
After `publish_code_suggestions()` is called in `_publish_key_issues_as_inline_comments`, the code creates a brand-new `InlineCommentStore` in `_published_inline_key_issue_fingerprints` and does a full fresh load of all PR/MR comments via provider API calls. If this listing call does not yet reflect the just-posted comment(s) (read-after-write staleness, or provider quirks), a successfully published finding will incorrectly be treated as unverified, causing it to be duplicated: it stays posted as an inline comment AND remains listed in the review summary.

## Issue Context
The function `_publish_key_issues_as_inline_comments` already loads and holds a `store` (an `InlineCommentStore`) before publishing. After publishing, instead of trusting that a successful `publish_code_suggestions()` call means the comments are live, the code performs an entirely separate, second `InlineCommentStore` load to verify. This doubles provider API calls and depends on eventual consistency of the listing endpoint.

## Fix Focus Areas
- pr_agent/tools/pr_reviewer.py[371-376]
- pr_agent/tools/pr_reviewer.py[422-440]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


16. Stale comments suppress findings ✓ Resolved 🐞 Bug ≡ Correctness
Description
An existing marker is treated as proof that the current finding is already published, but its
fingerprint contains no source content or revision identity. After code changes at the same numeric
line, an outdated comment with matching issue text can remove the regenerated finding from the new
summary without anchoring it to the current diff.
Code

pr_agent/tools/pr_reviewer.py[R405-408]

+                fingerprint = body_fingerprint(comment["relevant_file"], comment["relevant_lines_start"],
+                                               comment["body"])
+                if store.seen(fingerprint):
+                    published += 1
Relevance

●● Moderate

Team accepted some dedup hardening but previously rejected stronger fingerprint anchoring;
revision-aware markers may be contentious.

PR-#2424

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The changed path computes a fingerprint and immediately counts any store match as published. The
fingerprint implementation contains only relevant_file, target_line_no, and the first 80
normalized body characters, while existing-comment enumeration does not filter markers by current
revision or whether the comment is outdated.

pr_agent/tools/pr_reviewer.py[405-409]
pr_agent/algo/inline_comment_dedup.py[61-66]
pr_agent/algo/inline_comment_dedup.py[108-129]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Old inline comments can suppress a regenerated finding after the underlying code changes because key-issue fingerprints are not tied to the current source content or revision.

## Issue Context
The shared `body_fingerprint` hashes only path, numeric start line, and a body prefix. For inline key issues, include enough current anchor identity—such as the complete range and current `head_file` lines—to ensure a marker from changed code is not treated as a confirmed current publication.

## Fix Focus Areas
- pr_agent/tools/pr_reviewer.py[405-409]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


17. Same-run duplicate comments ✓ Resolved 🐞 Bug ☼ Reliability
Description
_publish_key_issues_as_inline_comments() can publish the same key issue multiple times in a single
run because it only checks store.seen(fingerprint) against already-existing PR comments and does not
reserve fingerprints while iterating. The fingerprint is only added to the store after verification,
so duplicated findings in the input list will each call publish_code_suggestions().
Code

pr_agent/tools/pr_reviewer.py[R404-412]

+                fingerprint = body_fingerprint(comment["relevant_file"], comment["relevant_lines_start"],
+                                               comment["body"])
+                if store.seen(fingerprint):
+                    published += 1
+                    continue
+                comment["body"] = body_with_markers(comment["body"], fingerprint, None,
+                                                     getattr(self.git_provider, "max_comment_chars", None))
+                self.git_provider.publish_code_suggestions([comment])
+                candidates.append((issue, fingerprint))
Evidence
The code checks the store (which reflects only previously-existing inline comments) before
publishing and appends each candidate, but only adds fingerprints to the store later during the
verification pass. With duplicate issues in the same input list, both will pass the initial
store.seen() check and both will be published.

pr_agent/tools/pr_reviewer.py[392-413]
pr_agent/tools/pr_reviewer.py[417-425]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When multiple key issues in the same run produce the same fingerprint (same file + start line + body), they will all be published because the dedup store is only updated after verification. This can produce duplicate inline comments in a single `/review` execution.

## Issue Context
The implementation uses the existing InlineCommentStore to prevent cross-run duplicates, but the store is only updated (`store.add`) after a second verification pass. There is no in-memory reservation set to prevent repeated publish attempts for identical candidates within the same run.

## Fix Focus Areas
- pr_agent/tools/pr_reviewer.py[392-426]

### Implementation sketch
- Introduce a local `attempted_fingerprints: set[str]` inside `_publish_key_issues_as_inline_comments`.
- After computing `fingerprint`, skip publishing if `fingerprint in attempted_fingerprints` (do not add a second candidate).
- Add `fingerprint` to `attempted_fingerprints` before calling `publish_code_suggestions`.
- Keep the existing verification logic so unverified candidates still remain in the summary.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


18. Suggestion fences not sanitized ✓ Resolved 🐞 Bug ≡ Correctness
Description
_build_key_issue_comment publishes model-controlled issue_content as an inline comment body without
stripping/escaping ```suggestion fences. If a key issue contains that fence, it is sent via
publish_code_suggestions and can be interpreted as a committable suggestion rather than a pure
observation.
Code

pr_agent/tools/pr_reviewer.py[R365-366]

+        body = f"**{issue_header}**\n\n{issue_content}" if issue_header else issue_content
+        return {"body": body,
...
  • Author self-review: I have reviewed the code review findings, and addressed the relevant ones.
Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread pr_agent/tools/pr_reviewer.py Outdated
Comment thread pr_agent/tools/pr_reviewer.py Outdated
Comment thread pr_agent/tools/pr_reviewer.py Outdated
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit ac0885e

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit c044ebd

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 7354f01

@TLA020
TLA020 force-pushed the feature/inline-key-issues branch from 7354f01 to 7910f65 Compare August 13, 2026 15:43
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 7910f65

@TLA020
TLA020 force-pushed the feature/inline-key-issues branch from 7910f65 to 739c128 Compare August 14, 2026 17:02
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 739c128

@IsmaelMartinez IsmaelMartinez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hi @TLA020, thanks for contributing. I left a couple of comments. Thanks again!

Comment thread pr_agent/tools/pr_reviewer.py Outdated
Comment thread pr_agent/tools/pr_reviewer.py Outdated
Comment thread pr_agent/tools/pr_reviewer.py Outdated
@TLA020
TLA020 force-pushed the feature/inline-key-issues branch from 739c128 to 85facda Compare August 16, 2026 22:34
Comment thread pr_agent/tools/pr_reviewer.py Outdated
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 85facda

@TLA020
TLA020 force-pushed the feature/inline-key-issues branch from 85facda to 54c194d Compare August 17, 2026 06:58
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 54c194d

@IsmaelMartinez

Copy link
Copy Markdown
Collaborator

Thanks, batching and the within-run dedup both look right, and the fingerprint tests are thorough.

The line anchor in key_issue_fingerprint breaks cross-run dedup, though. github_provider.py:533-535 notes that diff positions shift as a PR gains commits, which is why the fingerprint was path-and-content only. I checked: an unchanged finding dedups, but the same finding shifted one line by a commit above it republishes and the old comment stays. For the record I would decline Qodo's "Fingerprint lacks line anchor" — acting on it is what caused this.

Also, the load_failed check at pr_reviewer.py:374-375 is dead. I removed it and the suite still passes at 1772, because returning None and returning an empty set both route every candidate into remaining_issues, so a finding can still be posted inline and stay in the summary. Your test_new_comment_load_failure_keeps_findings_in_the_summary encodes that as expected, which is a fair choice: better to state it than to have a check that does nothing. The pre-publish check at :391-394 is the real one and is well covered.

@TLA020

TLA020 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

Hi @TLA020, thanks for contributing. I left a couple of comments. Thanks again!

No problem!

@TLA020
TLA020 force-pushed the feature/inline-key-issues branch from 54c194d to 7ac5368 Compare August 17, 2026 18:31
@TLA020 TLA020 changed the title feat(review): publish key issues as inline comments feat(azure): publish review findings inline Aug 17, 2026
@TLA020
TLA020 force-pushed the feature/inline-key-issues branch from 7ac5368 to 1b74eed Compare August 17, 2026 18:38
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 1b74eed

Comment thread pr_agent/tools/pr_reviewer.py Outdated
Comment thread pr_agent/tools/pr_reviewer.py Outdated
Comment thread pr_agent/git_providers/azuredevops_provider.py
@IsmaelMartinez

Copy link
Copy Markdown
Collaborator

Sorry, got a couple of things wrong earlier, both corrected in the threads above.

Cross-run fingerprint fix verified locally. Happy to take this as-is and do the follow-ups myself if you'd rather not keep iterating.

@TLA020
TLA020 force-pushed the feature/inline-key-issues branch from 1b74eed to 4717544 Compare August 18, 2026 06:09
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 4717544

@TLA020
TLA020 force-pushed the feature/inline-key-issues branch from 4717544 to 100cbc8 Compare August 18, 2026 06:29
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 100cbc8

@IsmaelMartinez IsmaelMartinez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for doing this rather than handing it over. Verified at 100cbc8d: collapsing the location key back to the content fingerprint fails both of your new tests, so the two-locations case is genuinely covered, and the capability probe change only touches this feature's own gate. Merged with current main locally, 1835 pass, CI green. Approving.

@IsmaelMartinez
IsmaelMartinez merged commit e402397 into The-PR-Agent:main Aug 18, 2026
5 checks passed
@TLA020

TLA020 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

No problem @IsmaelMartinez using it daily :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Publish Azure DevOps review findings as inline comments

2 participants