You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
pullRequestOutcomeState attributes a recommendation outcome based on what
happened to the matched PR after the action. The terminal "merged" branch
correctly requires the merge to post-date the action (mergedAt >= actionAt),
but the "accepted" fallback only requires a later updatedAt. So a PR that was merged before the recommendation — which fails the merged guard, and whose !pr.mergedAt short-circuit is false because mergedAt is set — falls through
to "accepted" the moment it receives any post-action update (e.g. a single
comment). "accepted" is a positive state (recommendation-quality-report.ts:74 POSITIVE_STATES = ["accepted", "merged", "improved"]), so the recommendation is
credited with a merge that causally predates it, inflating the positive rate.
Evidence
// src/services/recommendation-outcomes.ts:271functionpullRequestOutcomeState(pr,action,actionAt,actionAgeMs,staleAfterMs){constupdatedAt=timestamp(pr.updatedAt??pr.createdAt);constcreatedAt=timestamp(pr.createdAt??pr.updatedAt);constmergedAt=timestamp(pr.mergedAt);if((Number.isFinite(mergedAt)&&mergedAt>=actionAt)||(!pr.mergedAt&&pr.state==="merged"&&updatedAt>=actionAt))return"merged";if(pr.state==="closed"&&updatedAt>=actionAt)return"closed";if(action.targetPullNumber&&updatedAt>=actionAt){if(hasChangesRequestedReview(pr))return"rejected";if(hasPositiveOpenPullRequestSignal(pr))return"improved";}if(createdAt>=actionAt||updatedAt>actionAt)return"accepted";// <-- pre-action merge + later comment lands hereif(actionAgeMs>=staleAfterMs)return"stale";return"ignored";}
For a PR merged before the action: line 281's first clause is false
(mergedAt < actionAt); the second clause is false (!pr.mergedAt is false). If pr.state is "merged" (not "closed"), line 282 is skipped. So the PR reaches
line 287, and a post-action comment (updatedAt > actionAt) yields "accepted".
The intended semantics are already in the tests
test/unit/recommendation-outcomes.test.ts:233-247 asserts that exactly this
pre-action-merged PR — merged 2026-04-01, no post-action activity
(updatedAt == mergedAt) — is classified "stale" (a non-positive state):
So the design intent is that a merge predating the recommendation is not a
positive outcome. The bug is that a single post-action updatedAt bump (a
comment, a label, a base-branch update) flips that same PR from "stale" to the
positive "accepted".
Concrete trace
Action A recommends work on owner/old-merged at actionAt = 2026-05-01,
with targetPullNumber = 60 (an exact-PR match via recommendation-outcomes.ts:79).
Without the comment, the same PR is "stale" (per the test above). The
recommendation is now falsely scored as a positive ("accepted") outcome.
Impact
evaluateRecommendationOutcomes feeds the operator recommendation-quality
report; "accepted" is a positive state. Pre-existing merges that merely receive
incidental post-action activity are counted as recommendation wins, biasing the
quality/positive-rate metrics upward.
Test status
Not locked in. The merged-before-action case is only tested with updatedAt == mergedAt (no post-action activity → "stale"); no test exercises
a pre-action-merged PR that receives a later update.
Suggested fix
Guard the "accepted" fallback against a terminal state that predates the
action (mirroring the "merged" branch's own >= actionAt discipline):
Add a test: PR merged before actionAt with updatedAt > actionAt → "stale"/"ignored", not "accepted".
Distinct from prior reports
Same function as the closed #475 (which fixed a changes-requested PR being scored improved while merge-clean), but a different defect: the "accepted" fallback's
failure to exclude pre-action merges. #475 establishes that mis-classifications in pullRequestOutcomeState are treated as real bugs.
Confidence note
Reachability requires the exact-PR-target path (action.targetPullNumber) to
point at a PR that merged before the action and then received a later update — a
real but not the most common data shape. The internal inconsistency (terminal
branches guard the terminal event time; the accepted fallback does not) and the
test-revealed intent make it a genuine correctness defect rather than a modeling
choice.
Summary
pullRequestOutcomeStateattributes a recommendation outcome based on whathappened to the matched PR after the action. The terminal
"merged"branchcorrectly requires the merge to post-date the action (
mergedAt >= actionAt),but the
"accepted"fallback only requires a laterupdatedAt. So a PR that wasmerged before the recommendation — which fails the merged guard, and whose
!pr.mergedAtshort-circuit is false becausemergedAtis set — falls throughto
"accepted"the moment it receives any post-action update (e.g. a singlecomment).
"accepted"is a positive state (recommendation-quality-report.ts:74POSITIVE_STATES = ["accepted", "merged", "improved"]), so the recommendation iscredited with a merge that causally predates it, inflating the positive rate.
Evidence
For a PR merged before the action: line 281's first clause is false
(
mergedAt < actionAt); the second clause is false (!pr.mergedAtis false). Ifpr.stateis"merged"(not"closed"), line 282 is skipped. So the PR reachesline 287, and a post-action comment (
updatedAt > actionAt) yields"accepted".The intended semantics are already in the tests
test/unit/recommendation-outcomes.test.ts:233-247asserts that exactly thispre-action-merged PR — merged
2026-04-01, no post-action activity(
updatedAt == mergedAt) — is classified"stale"(a non-positive state):So the design intent is that a merge predating the recommendation is not a
positive outcome. The bug is that a single post-action
updatedAtbump (acomment, a label, a base-branch update) flips that same PR from
"stale"to thepositive
"accepted".Concrete trace
Arecommends work onowner/old-mergedatactionAt = 2026-05-01,with
targetPullNumber = 60(an exact-PR match viarecommendation-outcomes.ts:79).2026-03-01and merged2026-04-01(beforeA).2026-05-10someone comments →updatedAt = 2026-05-10.pullRequestOutcomeState:mergedAt (Apr 1) >= actionAt (May 1)→ false;!pr.mergedAt→ false;state === "closed"? if"merged", skipped → line287:
updatedAt (May 10) > actionAt (May 1)→"accepted"."stale"(per the test above). Therecommendation is now falsely scored as a positive ("accepted") outcome.
Impact
evaluateRecommendationOutcomesfeeds the operator recommendation-qualityreport;
"accepted"is a positive state. Pre-existing merges that merely receiveincidental post-action activity are counted as recommendation wins, biasing the
quality/positive-rate metrics upward.
Test status
Not locked in. The merged-before-action case is only tested with
updatedAt == mergedAt(no post-action activity →"stale"); no test exercisesa pre-action-merged PR that receives a later update.
Suggested fix
Guard the
"accepted"fallback against a terminal state that predates theaction (mirroring the
"merged"branch's own>= actionAtdiscipline):Add a test: PR merged before
actionAtwithupdatedAt > actionAt→"stale"/"ignored", not"accepted".Distinct from prior reports
Same function as the closed #475 (which fixed a changes-requested PR being scored
improvedwhile merge-clean), but a different defect: the"accepted"fallback'sfailure to exclude pre-action merges. #475 establishes that mis-classifications in
pullRequestOutcomeStateare treated as real bugs.Confidence note
Reachability requires the exact-PR-target path (
action.targetPullNumber) topoint at a PR that merged before the action and then received a later update — a
real but not the most common data shape. The internal inconsistency (terminal
branches guard the terminal event time; the
acceptedfallback does not) and thetest-revealed intent make it a genuine correctness defect rather than a modeling
choice.