Skip to content

Quiet PR-level findings: threads over prose, confidence over volume#80

Merged
mk7luke merged 2 commits into
mainfrom
fix/diffsentry-pr-noise
Jul 14, 2026
Merged

Quiet PR-level findings: threads over prose, confidence over volume#80
mk7luke merged 2 commits into
mainfrom
fix/diffsentry-pr-noise

Conversation

@mk7luke

@mk7luke mk7luke commented Jul 14, 2026

Copy link
Copy Markdown
Owner

The problem

Our own PRs got noisy. The "🔎 Issues not tied to a specific line" section was showing Major / Medium-confidence findings at the top of every review, and they couldn't be resolved, replied to, or collapsed — they just accumulated.

It traces to #76, which fixed something real: a REQUEST_CHANGES review could read "Actionable comments posted: 0" with nothing to act on, because findings with no anchorable diff line were being dropped. That invariant is right and is preserved here. The problem is the home it chose — prose in the review summary body, the one DiffSentry channel a reader can't dismiss — and four things funnelled into it:

  1. Drift was hardcoded to Major. reviewer.ts stamped severity: "major", confidence: "medium" on every warning-level drift finding, and isNitpick never consulted confidence — so all drift counted as actionable and claimed a slot at the top.
  2. The channel was wider than drift. parse.ts demoted any un-anchorable major/critical inline finding there — findings that wanted a line and missed the line number.
  3. They reprinted on every push. submitReview creates a new review per push, and cross-review dedup keys on fingerprintFor(path, line, title). PR-level findings have line: 0 and often no path, leaving the title as effectively the whole key — and drift titles are free prose regenerated per run. One re-wording minted a fresh fingerprint and the finding reappeared.
  4. Those reviews were never cleaned up. The dismissal pass only handled CHANGES_REQUESTED, but drift bumps APPROVE → COMMENT and never blocks — so the exact review class Fix "requested changes" reviews that name no actionable fix #76 introduced was the one class that stayed on the timeline forever.

The change

Route each finding to a channel based on what that channel costs the reader.

  • github.ts — Post file-scoped findings as real resolvable threads (subject_type: "file"). They post before the review so anything GitHub rejects folds back into the body, and a finding is never lost between channels. Retire our superseded reviews: dismiss CHANGES_REQUESTED, and stub COMMENTED bodies via updateReview, since GitHub refuses to dismiss those and submitted reviews can't be deleted. Match on our own body marker rather than user.type === "Bot" — the old filter reached other bots' reviews.
  • review-body.ts — Split prLevel by whether a path survived (thread-able vs. body-only). Gate the body section on high confidence; collapse the rest. isVisiblyActionable becomes the single source of truth for both the count and the REQUEST_CHANGES invariant, so the header number and the verdict can't disagree.
  • drift.ts / reviewer.ts — Ask the model for confidence and carry it through instead of hardcoding "medium". An omitted value defaults to "medium", not the "high" that ReviewComment assumes — drift is the class least entitled to the benefit of the doubt. Severity stays "major" deliberately: severity is how bad if real, confidence is whether it's real, and conflating them is what caused this. Only high-confidence drift may withhold an APPROVE.
  • parse.ts / walkthrough-state.tstitleSimilarity (Jaccard over content words, 0.6 threshold) catches re-worded repeats that fingerprinting misses, backed by a capped postedPrLevelKeys. Same-path-only, so a real finding is never swallowed by a same-sounding one about different code.

Verification

Rendered the exact review that prompted this (2 Major/Medium drift findings + 1 inline bug) through formatReviewBody:

  • "Actionable comments posted: 3" → 1
  • Both drift findings moved from the prominent section into the collapsed block — still present, no longer shouting
  • The inline bug stays a normal thread

Adds tests/unit/pr-level-noise.test.ts (29 tests) covering the confidence gate, the thread/body split, the reconcile invariant under gating, similarity dedup, drift confidence parsing, and submitReview's thread posting + review retirement. 361 unit tests pass; typecheck and lint clean (0 errors).

Worth knowing

DiffSentry is a GitHub App, so the subject_type: "file" call and the review-retirement path are covered by mocked-Octokit tests, not a live PR. Those are the two pieces to watch on the first real review after this merges — including on this PR itself.

🤖 Generated with Claude Code


Summary

Routed PR-level findings to resolvable file threads or confidence-gated body sections, and retired superseded DiffSentry reviews so unanchored drift no longer accumulates as permanent, unresolvable prose.

Changes

File Changes
src/ai/parse.ts Added Jaccard title-similarity helpers and same-path PR-level repeat detection so re-worded unanchored findings are deduped across reviews.
src/drift.ts Asked the model for drift confidence, defaulted omitted values to medium, and gated APPROVE→COMMENT on high-confidence drift only.
src/reviewer.ts Carried model confidence through drift findings, applied confidence-aware approval, and persisted a capped postedPrLevelKeys window for similarity dedup.
tests/unit/pr-level-noise.test.ts Added 29 unit tests covering confidence gates, thread/body split, reconcile invariant, title-similarity dedup, drift confidence parsing, and submitReview thread posting/retirement.

PR #76 gave findings with no anchorable diff line a home so a
REQUEST_CHANGES review could never read "Actionable comments posted: 0"
with nothing to act on. That invariant is right and stays. But the home it
chose — prose in the review summary body — is the one DiffSentry channel a
reader cannot resolve, reply to, or collapse, and four things funnelled
into it:

- Every warning-level drift finding was stamped `severity: "major",
  confidence: "medium"`, and isNitpick never consulted confidence, so all
  drift counted as actionable and claimed a Major slot at the top.
- parse.ts demoted ANY un-anchorable major/critical inline finding there —
  findings that wanted a line and missed.
- submitReview creates a new review per push, and the cross-review dedup
  keys on fingerprintFor(path, line, title). PR-level findings have line 0
  and often no path, leaving the title as the whole key — and drift titles
  are free prose regenerated per run. One re-wording minted a fresh
  fingerprint and the finding reprinted in every review body.
- The dismissal pass only handled CHANGES_REQUESTED, but drift bumps
  APPROVE→COMMENT and never blocks — so the exact review class #76
  introduced was the one class never cleaned up.

Route findings to a channel by what the channel costs the reader:

- github: post file-scoped findings as real resolvable threads
  (subject_type: "file"); post them BEFORE the review so any GitHub
  rejects fold back into the body and a finding is never lost between
  channels. Retire our superseded reviews — dismiss CHANGES_REQUESTED,
  and stub COMMENTED bodies via updateReview since GitHub refuses to
  dismiss them and submitted reviews can't be deleted. Match on our own
  body marker, not `user.type === "Bot"`, which reached other bots.
- review-body: split prLevel by whether a path survived (thread-able vs
  body-only); gate the body section on high confidence and collapse the
  rest; add isVisiblyActionable as the single source of truth for both the
  count and the REQUEST_CHANGES invariant, so the two can't disagree.
- drift: ask for confidence and carry it through instead of hardcoding
  "medium"; default an omitted value to "medium", not the "high"
  ReviewComment assumes. severity stays "major" — severity is how bad if
  real, confidence is whether it's real, and conflating them caused this.
  Only high-confidence drift may withhold an APPROVE.
- parse/state: titleSimilarity (Jaccard over content words, 0.6) catches
  re-worded PR-level repeats that fingerprinting misses, backed by a
  capped postedPrLevelKeys; same-path-only, so a real finding is never
  swallowed by a same-sounding one about different code.

On the review that prompted this, "Actionable comments posted: 3" becomes
1 and both Major/Medium drift findings collapse — still present, no longer
shouting. Adds tests/unit/pr-level-noise.test.ts (29 tests). 361 unit
tests pass, typecheck and lint clean.

The subject_type:"file" call and review retirement are covered by
mocked-Octokit tests, not a live PR — worth watching on the first real
review after this ships.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@diffsentry

diffsentry Bot commented Jul 14, 2026

Copy link
Copy Markdown

💬 DiffSentry has completed the review — Review complete with comments

@diffsentry

diffsentry Bot commented Jul 14, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Routed PR-level findings to resolvable file threads or confidence-gated body sections, and retired superseded DiffSentry reviews so unanchored drift no longer accumulates as permanent, unresolvable prose.

Changes

Cohort / File(s) Summary
PR-Level Dedup
src/ai/parse.ts, src/reviewer.ts
Added title-similarity dedup for re-worded PR-level findings and wired a bounded postedPrLevelKeys window through reviewer state so repeats stop reprinting across pushes.
Drift Confidence
src/drift.ts
Carried model confidence on drift findings with a medium default and limited APPROVE demotion to high-confidence warnings only.
Noise Control Tests
tests/unit/pr-level-noise.test.ts
Covered confidence gating, thread vs body routing, approval reconciliation, similarity dedup, drift confidence parsing, and review retirement behavior.

Sequence Diagram(s)

sequenceDiagram
    participant Reviewer
    participant Drift
    participant Parse
    participant ReviewBody
    participant GitHub
    Reviewer->>Drift: detectDescriptionDrift
    Drift-->>Reviewer: findings with confidence
    Reviewer->>Parse: isRepeatPrLevelFinding
    Parse-->>Reviewer: drop re-worded repeats
    Reviewer->>ReviewBody: formatReviewBody
    Note over ReviewBody: high conf body findings stay prominent
    Note over ReviewBody: medium/low collapse uncounted
    Reviewer->>GitHub: submitReview
    GitHub->>GitHub: post subject_type file threads
    GitHub->>GitHub: retire superseded DiffSentry reviews
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Suggested Labels

enhancement, refactor

Risk Assessment

Score: 3/100 — 🟢 Low

Factor Weight Detail
Large change +3 604 lines changed across 4 files

Test Coverage Signal

🟢 177 prod / 414 test lines added.

Source files changed Test files changed Source lines + Test lines +
3 1 177 414

Linked Issues

  • #76 — Fix "requested changes" reviews that name no actionable fix 🔴
🚥 Pre-merge checks | ✅ 5 | ❌ 0
✅ Passed checks (5 passed)
Check name Status Explanation
PR Title ✅ Passed Title is imperative ("Quiet…"), 68 chars (<72), no trailing period, and no Conventional Commits prefix.
PR Description ✅ Passed Description explains WHAT changed (route PR-level findings to file threads or confidence-gated body sections, retire superseded reviews) and WHY (unresolvable Major/Medium drift was accumulating and reprinting). Related issue/PR #76 is linked and contextualized.
Schema bump ✅ Passed src/storage/db.ts was not modified in this PR; no schema migration rules apply.
Provider parity ✅ Passed src/ai/anthropic.ts was not modified in this PR, so no request/response contract changes require corresponding updates in openai.ts or openai-compatible.ts.
Pattern test coverage ✅ Passed Neither src/safety-scanner.ts nor src/pattern-checks.ts was modified in this PR; no new rules to cover.

✏️ Tip: You can configure your own custom pre-merge checks in your .diffsentry.yaml.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Generate docstrings (beta)
  • Push docstring commit to this branch
🧹 Simplify (beta)
  • Push simplification commit to this branch
🪄 Autofix unresolved comments (beta)
  • Push autofix commit to this branch

Comment @diffsentry help to get the list of available commands and usage tips.

diffsentry[bot]
diffsentry Bot previously requested changes Jul 14, 2026

@diffsentry diffsentry 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

This PR cleanly routes PR-level findings by cost to the reader: file-scoped ones become resolvable threads, high-confidence body findings stay prominent, and the rest collapse. The confidence-gated isVisiblyActionable source of truth, similarity-based PR-level dedup, and superseded-review retirement all look solid and well-tested. One major gap remains: medium/low-confidence drift can still force APPROVE→COMMENT via reconcileApproval even though the new high-confidence gate was meant to stop that.

🔎 Issues not tied to a specific line (1)

These findings concern the change as a whole (for example, the diff versus the PR description) and can't be attached to a single changed line.

🛠️ Refactor suggestion | 🟡 Minor

Coverage is strong, but the approval-path regression needs a focused test.

The new unit file is excellent on rendering, similarity dedup, drift confidence parsing, and submitReview retirement/thread posting. What it does not lock down is the end-to-end approval interaction: a clean AI approval plus only medium-confidence drift should remain APPROVE after fold + reconcileApproval.

Add that case next to the existing reconcile tests so the main product promise of this PR cannot regress.

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

In tests/unit/pr-level-noise.test.ts, add an end-to-end-style case that builds a ReviewResult with approval APPROVE and only medium/low-confidence body-level drift findings, then runs the same fold/reconcile rules used in reviewer.ts and asserts the final approval remains APPROVE while the finding is collapsed/uncounted. Also assert a high-confidence drift finding still yields COMMENT.
🧹 Nitpick comments (4)
src/ai/parse.ts (1)

Line 206: Repeat matching ignores empty-path vs path-scoped drift synonyms only by exact path.

🛠️ Refactor suggestion | 🟡 Minor

Repeat matching ignores empty-path vs path-scoped drift synonyms only by exact path.

🤔 Medium confidence — verify against intent before acting.

isRepeatPrLevelFinding correctly refuses to match across different paths, which is good. One edge case still hurts the main noise source: drift findings are emitted with path: "", so a later reworded drift finding is only suppressed if the prior key also had an empty path. That is fine today, but if any future path-bearing PR-level finding reuses the same title prose, the empty-path drift repeat will not collapse against it and vice versa.

Not a blocker for this PR, but the key format makes path part of identity forever. Consider documenting that callers must keep drift unscoped (path: "") or normalizing both sides to the same scope before comparison.

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

In src/ai/parse.ts around isRepeatPrLevelFinding near line 206, document or normalize path scoping so empty-path PR-level findings only compare against other empty-path keys, and add a short comment/test clarifying that drift must continue to use path: "" for repeat suppression to work. No behavior change required unless you choose to intentionally allow empty-path candidates to match same-title path-scoped priors.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In src/ai/parse.ts around isRepeatPrLevelFinding near line 206, document or normalize path scoping so empty-path PR-level findings only compare against other empty-path keys, and add a short comment/test clarifying that drift must continue to use path: "" for repeat suppression to work. No behavior change required unless you choose to intentionally allow empty-path candidates to match same-title path-scoped priors.
src/github.ts (1)

Line 601: APPROVED DiffSentry reviews are left intact forever.

🛠️ Refactor suggestion | 🟡 Minor

APPROVED DiffSentry reviews are left intact forever.

🤔 Medium confidence — verify against intent before acting.

retireSupersededReviews only dismisses CHANGES_REQUESTED and stubs COMMENTED. Reviews that ended APPROVED still match REVIEW_BODY_MARKER and remain on the timeline with their full body.

That is probably acceptable, but after this PR a later push that finds medium-confidence drift can leave both an old green approval and a new commented review visible. If the goal is one current DiffSentry review body, consider stubbing APPROVED reviews the same way as COMMENTED, or explicitly document why approvals are retained.

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

In src/github.ts around retireSupersededReviews near line 601, decide whether APPROVED DiffSentry reviews should also be rewritten to SUPERSEDED_REVIEW_STUB like COMMENTED reviews. If yes, handle state === "APPROVED" via updateReview; if no, add a brief comment explaining that prior approvals are intentionally left visible.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In src/github.ts around retireSupersededReviews near line 601, decide whether APPROVED DiffSentry reviews should also be rewritten to SUPERSEDED_REVIEW_STUB like COMMENTED reviews. If yes, handle state === "APPROVED" via updateReview; if no, add a brief comment explaining that prior approvals are intentionally left visible.
src/review-body.ts (1)

Line 434: Actionable count includes file-level findings that later fail to post.

🛠️ Refactor suggestion | 🟡 Minor

Actionable count includes file-level findings that later fail to post.

🤔 Medium confidence — verify against intent before acting.

formatReviewBody counts file-level findings as actionable before submitReview tries to post them. If GitHub rejects the file thread and folds the finding into the fallback body section, the header can still say "Actionable comments posted: N" while the only visible copy is the non-threaded fallback block.

This is rare, but it reintroduces a softer version of the #76 inconsistency on the rejection path. Either recount after postFileLevelComments, or have the fallback section clearly count those items as posted-through-fallback.

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

In src/review-body.ts around formatReviewBody near line 434 and the submitReview fallback path in src/github.ts, reconcile the actionable count with unpostable file-level findings. Either format the body after postFileLevelComments returns, or ensure rejected file-level findings remain represented in a way that keeps the header count honest.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In src/review-body.ts around formatReviewBody near line 434 and the submitReview fallback path in src/github.ts, reconcile the actionable count with unpostable file-level findings. Either format the body after postFileLevelComments returns, or ensure rejected file-level findings remain represented in a way that keeps the header count honest.
src/ai/prompt.ts (1)

Line 40: Prompt growth is justified; keep the confidence guidance tight.

🧹 Nitpick | 🟢 Trivial

Prompt growth is justified; keep the confidence guidance tight.

🤔 Low confidence — flagging as a hypothesis; may not apply.

The new prLevelComments cost/confidence sentence is the right lever and should reduce expensive body findings. It is still prompt growth on every review. Given the measured win described in the PR, this looks worth it, but watch for models over-collapsing real cross-cutting issues into medium confidence after this ships.

No code change required unless live reviews start under-reporting genuine PR-level blockers.

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

In src/ai/prompt.ts around line 40, leave the new prLevelComments confidence guidance in place for now. After the first few live reviews, check whether high-confidence PR-level blockers are being under-emitted and tighten the wording only if that regression appears.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In src/ai/prompt.ts around line 40, leave the new prLevelComments confidence guidance in place for now. After the first few live reviews, check whether high-confidence PR-level blockers are being under-emitted and tighten the wording only if that regression appears.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

In `src/github.ts`:
- Line 650: In src/github.ts around postFileLevelComments (near line 650) and its submitReview call site, stop unconditionally creating a new file-scoped review comment for every file-level finding. Reuse the existing fingerprint / postedPrLevelKeys dedup already computed in reviewer.ts, or check previously posted DiffSentry file comments on the same path, and skip re-posting repeats so successive pushes do not stack duplicate file threads.
- Line 601: In src/github.ts around retireSupersededReviews near line 601, decide whether APPROVED DiffSentry reviews should also be rewritten to SUPERSEDED_REVIEW_STUB like COMMENTED reviews. If yes, handle state === "APPROVED" via updateReview; if no, add a brief comment explaining that prior approvals are intentionally left visible.

In `src/ai/parse.ts`:
- Line 206: In src/ai/parse.ts around isRepeatPrLevelFinding near line 206, document or normalize path scoping so empty-path PR-level findings only compare against other empty-path keys, and add a short comment/test clarifying that drift must continue to use path: "" for repeat suppression to work. No behavior change required unless you choose to intentionally allow empty-path candidates to match same-title path-scoped priors.

In `src/review-body.ts`:
- Line 434: In src/review-body.ts around formatReviewBody near line 434 and the submitReview fallback path in src/github.ts, reconcile the actionable count with unpostable file-level findings. Either format the body after postFileLevelComments returns, or ensure rejected file-level findings remain represented in a way that keeps the header count honest.

In `src/ai/prompt.ts`:
- Line 40: In src/ai/prompt.ts around line 40, leave the new prLevelComments confidence guidance in place for now. After the first few live reviews, check whether high-confidence PR-level blockers are being under-emitted and tighten the wording only if that regression appears.
🪄 Autofix (Beta)

Fix all unresolved DiffSentry comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: .diffsentry.yaml

Review profile: ASSERTIVE

Run ID: 8d28e42b-4388-421b-b318-3bd244accd0c

📥 Commits

Reviewing files at 3cfdacf (base SHA unavailable).

📒 Files selected for processing (8)
  • src/ai/parse.ts
  • src/ai/prompt.ts
  • src/drift.ts
  • src/github.ts
  • src/review-body.ts
  • src/reviewer.ts
  • src/walkthrough-state.ts
  • tests/unit/pr-level-noise.test.ts
✅ Files skipped from review due to trivial changes (1)
  • src/types.ts

Comment thread src/github.ts
Comment thread src/ai/parse.ts
Comment thread src/github.ts
Comment thread src/review-body.ts
Comment thread src/ai/prompt.ts
@diffsentry

diffsentry Bot commented Jul 14, 2026

Copy link
Copy Markdown

📌 Status — last updated bd43de4

🟡 Comments only

Risk score 3/100 (Low) █▁
Unresolved threads 0
Failing checks 0
Pending checks 1
Files reviewed 4 (5 skipped)
Updated 2026-07-14 19:02Z

Live-updated by DiffSentry on every push. Use @diffsentry ship for a verdict, @diffsentry timeline for full history.

Extract the drift approval gate into applyDriftToApproval (drift.ts) so the
rule that hedged drift must not cost an approval is testable in isolation
rather than buried inline in the reviewer's main method. No behaviour
change — same condition, now pinned by tests.

Adds 8 tests: the APPROVE + medium/low-drift path end to end through both
verdict rules, and coverage proving isRepeatPrLevelFinding spans file-level
findings (not just body-level ones) so re-worded file findings can't stack
duplicate threads across pushes.

Documents the path-scope contract on isRepeatPrLevelFinding: path is part of
a finding's identity, comparison is same-scope only, and drift must keep
emitting with path: "" to collapse against its own history.

369 unit tests pass, typecheck and lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@diffsentry diffsentry Bot dismissed their stale review July 14, 2026 19:02

Superseded by new review.

@diffsentry diffsentry 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: 0

This PR addresses the PR-level noise problem thoughtfully: the confidence gating, threaded file-scoped findings, and superseded-review retirement all move the system in the right direction. I found one correctness issue in the new title-similarity dedupe path: prLevelRepeatKey uses a raw tab-delimited serialization, so titles containing tabs can corrupt parsing and cause missed or incorrect repeat suppression. Aside from that, the tests are strong and the overall design is coherent.


ℹ️ Review info
⚙️ Run configuration

Configuration used: .diffsentry.yaml

Review profile: ASSERTIVE

Run ID: d3e469bc-3cf7-418d-b13b-87c9924c0587

📥 Commits

Reviewing files that changed from 3cfdacf to bd43de4. Previously-reviewed commits are not re-reviewed.

📒 Files selected for processing (4)
  • src/ai/parse.ts
  • src/drift.ts
  • src/reviewer.ts
  • tests/unit/pr-level-noise.test.ts
✅ Files skipped from review due to trivial changes (1)
  • src/types.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/ai/prompt.ts
  • src/github.ts
  • src/review-body.ts
  • src/walkthrough-state.ts

ℹ️ The primary review model was unavailable; this review was generated by the configured backup provider.

@mk7luke mk7luke merged commit 89c12e4 into main Jul 14, 2026
4 checks passed
@mk7luke mk7luke deleted the fix/diffsentry-pr-noise branch July 14, 2026 19:05
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.

1 participant