Skip to content

fix(findings): stop counting human resolution replies as findings - #162

Open
topcoder1 wants to merge 3 commits into
fix/automerge-quiet-anchor-review-triggerfrom
fix/findings-reply-narrowing
Open

fix(findings): stop counting human resolution replies as findings#162
topcoder1 wants to merge 3 commits into
fix/automerge-quiet-anchor-review-triggerfrom
fix/findings-reply-narrowing

Conversation

@topcoder1

Copy link
Copy Markdown
Owner

Manual merge (ci-workflows policy). Stacked on #161 — based on its branch so the diff shows only this change; GitHub retargets to main when #161 merges. Merge order: #161 first, then this.

Paired PR: the detector is a vendored twin — the byte-identical body ships to topcoder1/dotclaude (templates/ci-workflows/scripts/bb-unaddressed-findings.sh) in dotclaude's fix/findings-reply-narrowing PR. Merge both or neither; identity below the header verified both directions.

HUMAN_READABLE_SUMMARY: The findings detector flagged the author's own "Fixed in <sha>" inline replies as unaddressed findings, because a resolution reply always post-dates the commit it cites. It now skips inline comments that are both threaded replies and human-authored. Human top-level comments and bot replies still count.


The defect, measured on wxa_vpn#1523

The report's top three hits were the PR's own "Fixed in 38774043" replies, posted 7–12s after the fix commit they cited. Across the PR: 5/5 false positives carried in_reply_to_id; 0/14 genuine bot findings did. A resolution reply necessarily post-dates the commit it cites — author pushes, then replies citing the push — so every resolution round trips the newer-than-last-commit signal, forever.

This reaches the automerge gate too: the quiet gate runs this detector and quotes its report verbatim into Auto-merge withheld comments. A false flag there declines arms on clean PRs, and a gate that blocks passing PRs gets switched off.

The narrowing

select(.in_reply_to_id == null or ((.user.login // "") | endswith("[bot]")))

Kept deliberately: human top-level inline comments (author-agnosticism — a human's own comment on the diff is a deliberate finding) and bot replies ("still broken after the fix" is a live finding; keeping them cost zero false positives in the corpus). Accepted residual: a human typing a new finding into an existing thread is skipped — the operator wrote it, so the operator knows it.

Tests — proven to bite

selftest/test_findings_reply_narrowing.sh (6 checks), CI-wired via _SHELL_SELFTESTS. Three mutations, each caught by its intended cases:

Mutation Result
remove the narrowing (pre-fix state) R1 + R4 fail — the #1523 shape flags again
drop the bot escape (exclude all replies) R2 fails
over-correct to bots-only inline R3 + R5 fail

R5 pins that a field-absent comment (legacy fixtures; REST omits the field on top-level comments) reads the same as null. Full wrapper suite: 46 pass. shellcheck clean on both files. dotclaude side: 3 new regression cases, mutation-proven there too (the bot-reply guard survives the removal mutation by design — it pins the over-correction direction).

🤖 Generated with Claude Code

wxa_vpn#1392's false-positive half, measured on wxa_vpn#1523: the report's top
three hits were the PR's own "Fixed in 38774043" inline replies, posted 7-12s
after the fix commit they cited. A reply that RESOLVES a finding necessarily
post-dates the commit it cites — the author pushes, then replies citing the
push — so every resolution round tripped the newer-than-last-commit signal and
the report re-flagged resolved threads forever. Across that PR: 5/5 false
positives carried in_reply_to_id; 0/14 genuine bot findings did.

The narrowing drops inline comments that are BOTH a reply AND human:

  select(.in_reply_to_id == null or ((.user.login // "") | endswith("[bot]")))

Untouched on purpose: human TOP-LEVEL inline comments still count (a human's
own comment on the diff is a deliberate finding — author-agnosticism survives),
and BOT replies still count (a reviewer bot answering a thread with "still
broken after the fix" is a live finding; keeping them cost zero false positives
in the measured corpus). Accepted residual: a human typing a NEW finding into
an existing thread is skipped — the operator wrote it, so the operator knows it.

This matters beyond the operator CLI because the automerge quiet gate runs this
detector and quotes its report verbatim into auto-merge-withheld comments — a
false flag there declines arms on clean PRs, and a gate that blocks passing PRs
gets switched off.

Vendored-twin discipline: the same edit ships to dotclaude's
templates/ci-workflows/scripts/bb-unaddressed-findings.sh in a paired PR;
byte-identity below the header block verified both ways.

Tests: selftest/test_findings_reply_narrowing.sh (6 checks), CI-wired via
_SHELL_SELFTESTS. Proven to bite by mutation: removing the narrowing fails
R1+R4 (the #1523 shape, rc=1 where 0 expected); dropping the bot escape fails
R2; over-correcting to bots-only inline fails R3+R5. Restored, all green;
shellcheck clean on both files.

Stacked on fix/automerge-quiet-anchor-review-trigger (#161) because both PRs
touch _SHELL_SELFTESTS — review only the top commit until #161 merges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 15, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@github-actions github-actions Bot added the risk:sensitive Risk class: sensitive label Aug 15, 2026
@github-actions

Copy link
Copy Markdown

Codex review

Coverage exercises the narrowed reply behavior, new test-state mutations are asserted via exit status, and function comments remain consistent.
Coverage exercises the narrowed reply behavior, new test-state mutations are asserted via exit status, and function comments remain consistent.

Model: gpt-5.6-sol · CLI: codex-cli 0.147.0 · reasoning: low

@github-actions

Copy link
Copy Markdown

Coverage Floor — mode: enforce

metric value
measured 100.0%
floor (current) 99.0%
target 100.0%
last bumped 2026-05-12

Comment thread .github/scripts/unaddressed-findings.sh Outdated
[ .[] | select(.created_at > $last) ]
[ .[] | select(.created_at > $last)
| select(.in_reply_to_id == null
or ((.user.login // "") | endswith("[bot]"))) ]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: null .user on a bot reply causes a false negative

When GitHub returns .user: null for a deleted or suspended account, a comment that is a threaded reply (in_reply_to_id non-null) silently fails both branches of the or and is excluded as if it were a human reply:

  1. .in_reply_to_id == null → false (it is non-null)
  2. (.user.login // "") → jq null-propagates .user.login to null, then null // "" = "", and "" | endswith("[bot]") → false

Result: a genuine bot finding posted as a threaded reply ("still broken after the fix") is dropped with no error. The 2>/dev/null ensures jq emits no diagnostic.

The corpus data (0/14 real bot findings were replies) makes this rare, but a bot reply that lands while the account is being rotated/suspended is exactly the scenario the bot-reply keep-rule exists for.

Suggested change
or ((.user.login // "") | endswith("[bot]"))) ]
| select(.in_reply_to_id == null
or (((.user // {}).login // "") | endswith("[bot]"))) ]

(.user // {}).login avoids the null-propagation path while keeping the // "" fallback for a null login field on an existing user object.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 583241d — null-user replies now count (drops KNOWN-human replies only, new R2b + S1 arm), explicit-null pinned as R6, and R4 rides the shared run_case harness via its exposed $OUT (R4/R4b). Twin transplanted to dotclaude byte-identically.

# payloads omit the field on top-level comments rather than null-ing it).
run_case 1 "R5 field-absent inline comment keeps today's behavior (rc=1)" '[
{"created_at":"'"$AFTER"'","user":{"login":"topcoder1"},"path":"deploy.yml",
"body":"Real bug here."}]'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Test gap: R5 only tests field-absent, never explicit "in_reply_to_id": null

R5's stated purpose is proving "absent and null must read the same." But both R3 and R5 use fixtures where the key is entirely absent — neither tests a payload with "in_reply_to_id": null (explicit JSON null).

The distinction matters: == null treats absent and explicit null identically, but a future rewrite to has("in_reply_to_id") | not would handle them differently — has() returns true when the key exists even with a null value, so an explicit-null comment would be excluded as a reply instead of flagged as a finding. No R* fixture would catch that regression; S1's source grep only guards the literal == null text.

GitHub's REST API omits in_reply_to_id for top-level comments rather than setting it to null, so the immediate risk is low — but the intent documented in the comment is to cover both spellings.

Add a case:

run_case 1 "R5b explicit in_reply_to_id:null is not a reply (rc=1)" '[
  {"created_at":"'"$AFTER"'","user":{"login":"topcoder1"},"path":"deploy.yml",
   "in_reply_to_id":null,
   "body":"Real bug here — explicit null."}]'

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 583241d — null-user replies now count (drops KNOWN-human replies only, new R2b + S1 arm), explicit-null pinned as R6, and R4 rides the shared run_case harness via its exposed $OUT (R4/R4b). Twin transplanted to dotclaude byte-identically.

{"created_at":"'"$AFTER"'","user":{"login":"claude[bot]"},"path":"docker-compose.yml",
"body":"The compose rationale is now inverted by the trust change."}]' > "$FX/inline.json"
set +e
OUT=$(bash "$CHECKER" --fixture "$FX" o/r 1 2>&1); RC=$?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Duplication: R4 hand-rolls the run_case harness because run_case doesn't expose $OUT

R4 needs to grep the output body (to verify it names the bot line and omits the human reply), so it re-implements the set +e / OUT=$(bash ...) / RC=$? / set -e block that run_case already encapsulates. If the checker path, the --fixture signature, or the 2>&1 redirect ever changes, R4's copy must be updated independently — with no cross-reference to point the editor there.

run_case could accept an optional fourth argument — a body-grep pattern — and do its own grep -q check internally, or it could expose $OUT via a nameref or a global. Either approach would let R4 be expressed as a single run_case call with an extra assertion, keeping all five cases uniform:

run_case_body() {  # $1 expected-rc, $2 name, $3 inline-json, $4 must-match, $5 must-not-match
  printf '%s' "$3" > "$FX/inline.json"
  set +e; OUT=$(bash "$CHECKER" --fixture "$FX" o/r 1 2>&1); RC=$?; set -e
  local ok=1
  [ "$RC" -eq "$1" ] || ok=0
  [ -z "${4:-}" ] || printf '%s' "$OUT" | grep -q "$4" || ok=0
  [ -z "${5:-}" ] || { printf '%s' "$OUT" | grep -q "$5" && ok=0; }
  if [ "$ok" -eq 1 ]; then echo "$2"; else
    echo "$2 — rc=$RC, expected $1"; printf '%s\n' "$OUT" | head -6 | sed 's/^/    /'; failed=1; fi
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 583241d — null-user replies now count (drops KNOWN-human replies only, new R2b + S1 arm), explicit-null pinned as R6, and R4 rides the shared run_case harness via its exposed $OUT (R4/R4b). Twin transplanted to dotclaude byte-identically.

@claude

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown

Flagged 3 issues inline — 1 production null-user bug in unaddressed-findings.sh, 1 test gap (explicit-null fixture missing), and 1 test-harness duplication in R4.

…it-null + harness cleanups

Three findings from #162's Claude Review, all addressed:

1. PRODUCTION: a reply whose .user is null (deleted/suspended account) read
   login "" via `// ""`, failed the [bot] test, and was DROPPED — an unknown
   author silently converted into a false negative. The exclusion now drops
   KNOWN-human replies only: `.in_reply_to_id == null or (.user.login == null)
   or (.user.login | endswith("[bot]"))`. The report's login column renders
   "-" for a null login instead of jq's "null". New R2b pins it; mutation
   (restoring the `// ""` spelling) fails R2b + S1.

2. TEST GAP: R5 covered field-absent only; new R6 covers the explicit
   `"in_reply_to_id": null` spelling, so a future has()-based rewrite cannot
   split the two.

3. HARNESS: run_case now documents that it leaves $OUT for content
   assertions and returns the case verdict; R4 uses it instead of
   hand-rolling a second harness (rc via R4, report content via R4b).

Twin transplanted to dotclaude byte-identically (verified); its regression
suite grew the matching two cases. shellcheck clean; all sibling selftests
pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Codex review

The changed behavior is covered, introduces no production state mutation, and matches the updated function comments.
The changed behavior is covered, introduces no production state mutation, and matches the updated function comments.

Model: gpt-5.6-sol · CLI: codex-cli 0.147.0 · reasoning: low

}

# R1. THE #1523 SHAPE.
run_case 0 "R1 human 'Fixed in <sha>' reply is not a finding (rc=0)" '[

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: set -euo pipefail + bare run_case calls — test exits on first failure, all subsequent cases skipped

run_case returns 1 when the checker's actual exit code doesn't match the expected one. This call (and R2, R2b, R3, R5, R6 at lines 105, 111, 117, 141, 148) is a bare statement under set -e, so bash treats the non-zero return as a fatal error and exits immediately. The remaining test cases are never run, failed stops accumulating, and the "FAIL — findings reply narrowing" summary at line 157 never prints. CI gets the right signal (non-zero exit) but the diagnostic output is cut short.

R4 at line 124 is correctly guarded with if run_case ...; then and doesn't share this problem.

Fix — use || to absorb the return without exiting:

Suggested change
run_case 0 "R1 human 'Fixed in <sha>' reply is not a finding (rc=0)" '[
run_case 0 "R1 human 'Fixed in <sha>' reply is not a finding (rc=0)" '[
{"created_at":"'"$AFTER"'","user":{"login":"topcoder1"},"path":"infra/x.yml",
"in_reply_to_id":3787072809,
"body":"Fixed in 38774043 — the docstring now records the inversion."}]' || true

Apply the same || true (or || :) pattern to the R2, R2b, R3, R5, R6 calls. The failed=1 inside run_case accumulates the count before returning, so the final exit "$failed" still reflects all failures.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in c8ef8ee — run_case always returns 0 under set -e (the R7 mutation run now prints all 11 cases and still exits FAIL), R4b runs unconditionally against $OUT, and S1 greps each arm independently with no -A window. The two top-level findings (late_issue null guard) are also in c8ef8ee: guarded with // "", crash pinned by R7 (mutation-proven), direction pinned by R8; twin transplanted byte-identically.

# R4. Mixed: replies must not mask (or appear beside) the real finding.
# Exit code through the shared harness; the report-content assertions read
# the $OUT it leaves behind.
if run_case 1 "R4 mixed thread flags (rc=1)" '[

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: R4b content assertions silently skipped when R4 fails

R4b — the only test that checks report content (bot finding appears, human reply does not) — is nested inside the same if run_case 1 ...; then block that gates on R4's exit code. If a bug causes check_pr to exit with rc=2 instead of rc=1 (e.g., a fixture error or an assertion in the checker), run_case returns 1, the outer if is false, and the two grep -q assertions at lines 129-130 are silently skipped. failed is not incremented for that gap, and the test reports PASS for a regression that lets human replies appear in the report.

Consider promoting R4b to an unconditional check, or at minimum incrementing failed when R4 itself fails in a way that prevents R4b from running:

if run_case 1 "R4 mixed thread flags (rc=1)" '[
  {"created_at":"'"$AFTER"'","user":{"login":"topcoder1"},"path":"infra/x.yml",
   "in_reply_to_id":9,"body":"Fixed in 38774043."},
  {"created_at":"'"$AFTER"'","user":{"login":"claude[bot]"},"path":"docker-compose.yml",
   "body":"The compose rationale is now inverted by the trust change."}]'; then
  if printf '%s' "$OUT" | grep -q "compose rationale" \
     && ! printf '%s' "$OUT" | grep -q "Fixed in 38774043"; then
    echo "✓ R4b report names the bot finding and omits the human replies"
  else
    echo "✗ R4b report must name the bot line and omit the replies:"
    printf '%s\n' "$OUT" | head -6 | sed 's/^/    /'
    failed=1
  fi
else
  echo "✗ R4b skipped — R4 itself failed; cannot verify report content"
  failed=1
fi

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in c8ef8ee — run_case always returns 0 under set -e (the R7 mutation run now prints all 11 cases and still exits FAIL), R4b runs unconditionally against $OUT, and S1 greps each arm independently with no -A window. The two top-level findings (late_issue null guard) are also in c8ef8ee: guarded with // "", crash pinned by R7 (mutation-proven), direction pinned by R8; twin transplanted byte-identically.

# escape would eat R2 silently on fixtures that never exercise bot replies;
# dropping the unknown-author arm would eat R2b the same way.
if grep -q 'in_reply_to_id == null' "$CHECKER" \
&& grep -A2 'in_reply_to_id == null' "$CHECKER" | grep -q '\.user\.login == null' \

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

PLAUSIBLE: S1's grep -A2 window is fragile — reformatting or a 4th condition triggers a false alarm

The three or arms in the checker's jq filter currently occupy exactly 3 lines, so grep -A2 (which prints the matching line plus 2 after it) just barely captures all three. If a formatter inserts a blank line between the second and third arms, or if a fourth condition is added on its own line, endswith("[bot]") falls outside the window and S1 fails — not because the logic is wrong, but because the window is too narrow.

Since R2 and R2b already exercise the bot-reply and null-user paths behaviorally, S1's value is mainly as a named-shape pin. Consider anchoring it on the whole block with a wider context or a single grep -c count, or simply drop S1 and rely on R2/R2b to catch arm-removal regressions.

Alternative that avoids the line-count fragility:

if grep -q 'in_reply_to_id == null' "$CHECKER" \
   && grep -q '\.user\.login == null' "$CHECKER" \
   && grep -q 'endswith("\[bot\]")' "$CHECKER"; then

(Three independent greps — order and co-location are not checked, but R2/R2b behaviorally cover those invariants.)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in c8ef8ee — run_case always returns 0 under set -e (the R7 mutation run now prints all 11 cases and still exits FAIL), R4b runs unconditionally against $OUT, and S1 greps each arm independently with no -A window. The two top-level findings (late_issue null guard) are also in c8ef8ee: guarded with // "", crash pinned by R7 (mutation-proven), direction pinned by R8; twin transplanted byte-identically.

@claude

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown

Flagged 5 issues (3 inline) — 2 additional findings on unchanged lines couldn't be posted inline: late_issue in unaddressed-findings.sh lacks the same null guard on .user.login that this PR added to late_inline (lines 633/640), which can cause a type error that silences all findings and exits 0.

…d; suite no longer aborts mid-run

Round 2 of #162's review flagged 5, all addressed:

1. PRODUCTION (the big one, pre-existing): late_issue's
   `select(.user.login | endswith("[bot]"))` was a jq TYPE ERROR on a
   null-user comment (deleted/suspended account). With stderr discarded the
   abort emptied late_issue and silenced EVERY top-level finding on the PR —
   exit 0 — because one unrelated comment lost its author. Now guarded with
   `// ""`: an unknown author falls to the path's DEFAULT (top-level counts
   KNOWN bots only — the mirror of inline, whose default is to count, so
   its unknown authors do). R7 pins the crash case (mutation: reverting the
   guard fails R7 with rc=0 — the silenced-findings behavior exactly); R8
   pins the direction. The TSV login column renders "-" for null.

2. HARNESS under set -e: run_case's round-2 `return 1` made a bare call
   abort the suite at the first failing case — my own mutation output was
   truncated by exactly this and I read it as complete. run_case now always
   returns 0; the verdict lives in `failed`. Proven: the R7 mutation run
   prints all 11 cases and still exits FAIL.

3. R4b content assertions now run UNCONDITIONALLY — gating them on R4's rc
   skipped the only content check exactly when the checker misbehaves.

4. S1 greps each arm independently instead of through a -A line window that
   broke on any reformat.

Twin transplanted to dotclaude byte-identically; its suite grew the two
matching cases (55 pass). shellcheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Codex review

Relevant tests pass; new test-state mutations are asserted through the suite exit status, and check_pr remains consistent with its comments.
Relevant tests pass; new test-state mutations are asserted through the suite exit status, and check_pr remains consistent with its comments.

Model: gpt-5.6-sol · CLI: codex-cli 0.147.0 · reasoning: low

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

Labels

risk:sensitive Risk class: sensitive

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant