Summary
classifyRecommendationOutcome can turn an explicit negative GitHub review signal into a positive recommendation outcome. In pullRequestOutcomeState, the classifier treats either approval or clean mergeability as a positive open-PR signal, then returns "improved" before it checks for "CHANGES_REQUESTED":
// src/services/recommendation-outcomes.ts:281-285
if ((Number.isFinite(mergedAt) && mergedAt >= actionAt) || (!pr.mergedAt && pr.state === "merged" && updatedAt >= actionAt)) return "merged";
if (pr.state === "closed" && updatedAt >= actionAt) return "closed";
const positiveOpenSignal = pr.reviewDecision === "APPROVED" || pr.mergeableState === "clean";
if (action.targetPullNumber && positiveOpenSignal && updatedAt >= actionAt) return "improved";
if (action.targetPullNumber && pr.reviewDecision === "CHANGES_REQUESTED" && updatedAt >= actionAt) return "rejected";
That means a targeted PR with:
reviewDecision: "CHANGES_REQUESTED",
mergeableState: "clean",
updatedAt: <after recommendation>
is classified as "improved", not "rejected".
mergeableState: "clean" only says the branch can merge technically. It does not override a reviewer requesting changes. The explicit review decision should win.
Failure mode
A miner receives a recommendation to work on owner/repo#123. After the recommendation, the PR is still open and GitHub reports:
reviewDecision = "CHANGES_REQUESTED"
mergeableState = "clean"
updatedAt >= action.createdAt
Current classifier path:
- PR is not merged.
- PR is not closed.
positiveOpenSignal = false || true, because mergeableState === "clean".
- The function returns
"improved".
- The later
CHANGES_REQUESTED branch is never reached.
This corrupts the recommendation-quality feedback loop:
improved is counted as positive in recommendation-quality-report.ts (POSITIVE_STATES)
rejected is counted as negative (NEGATIVE_STATES)
- failure categories miss the "Changes requested" row
- per-repo/top-repo signals can flip from negative to positive
Concrete reproduction
Call classifyRecommendationOutcome with:
- action:
targetRepoFullName: "owner/rejected-clean", targetPullNumber: 200, createdAt: "2026-05-01T00:00:00.000Z"
- PR record: same repo/number,
state: "open", updatedAt: "2026-05-10T00:00:00.000Z", reviewDecision: "CHANGES_REQUESTED", mergeableState: "clean"
- evaluation:
evaluatedAt: "2026-06-01T00:00:00.000Z"
Expected outcome:
{ outcomeState: "rejected" }
Actual outcome:
{ outcomeState: "improved" }
Test status
Partially covered, but the conflicting-signal case is missing.
Existing tests prove each individual branch works in isolation:
// test/unit/recommendation-outcomes.test.ts:414-421
reviewDecision: "CHANGES_REQUESTED" // no clean mergeability -> rejected
// test/unit/recommendation-outcomes.test.ts:441-448
reviewDecision: "APPROVED" // approved -> improved
// test/unit/recommendation-outcomes.test.ts:345-348
mergeableState: "clean" // no changes-requested review -> improved
No fixture combines reviewDecision: "CHANGES_REQUESTED" with mergeableState: "clean", so the precedence bug is not locked in.
Expected behavior
For targeted PR outcomes, negative review decisions should take precedence over mergeability-only positive signals:
- merged after recommendation ->
"merged"
- closed after recommendation ->
"closed"
CHANGES_REQUESTED after recommendation -> "rejected"
APPROVED after recommendation -> "improved"
- clean mergeability with no changes-requested review ->
"improved"
Actual behavior
mergeableState === "clean" is folded into positiveOpenSignal and checked before CHANGES_REQUESTED, so a technically merge-clean PR with requested changes is reported as improved.
Suggested fix
Prioritize explicit negative review decisions before mergeability-based improvement:
const hasTargetPullNumber = Boolean(action.targetPullNumber);
const hasRecentUpdate = updatedAt >= actionAt;
const reviewDecision = pr.reviewDecision?.toUpperCase();
const mergeableState = pr.mergeableState?.toLowerCase();
if (hasTargetPullNumber && reviewDecision === "CHANGES_REQUESTED" && hasRecentUpdate) return "rejected";
const positiveOpenSignal = reviewDecision === "APPROVED" || mergeableState === "clean";
if (hasTargetPullNumber && positiveOpenSignal && hasRecentUpdate) return "improved";
Add fail-on-revert coverage to test/unit/recommendation-outcomes.test.ts: a targeted open PR with both reviewDecision: "CHANGES_REQUESTED" and mergeableState: "clean" must classify as "rejected", not "improved".
Distinct from prior reports
This is not a duplicate of the earlier changes-requested bug in gittensory-philluiz-issue3.md.
- issue 3 is about
src/signals/local-branch.ts, where stale PR classification runs before blocked PR classification and inflates score-preview scenarios.
- this issue is about
src/services/recommendation-outcomes.ts, where a targeted PR's feedback outcome is marked positive (improved) before the explicit negative review state (rejected) can be observed.
The blast radius is different too: issue 3 affects scenario/open-PR pressure projections; this one affects recommendation-quality history, private summaries, role/report rollups, and the agent feedback loop.
Summary
classifyRecommendationOutcomecan turn an explicit negative GitHub review signal into a positive recommendation outcome. InpullRequestOutcomeState, the classifier treats either approval or clean mergeability as a positive open-PR signal, then returns"improved"before it checks for"CHANGES_REQUESTED":That means a targeted PR with:
is classified as
"improved", not"rejected".mergeableState: "clean"only says the branch can merge technically. It does not override a reviewer requesting changes. The explicit review decision should win.Failure mode
A miner receives a recommendation to work on
owner/repo#123. After the recommendation, the PR is still open and GitHub reports:reviewDecision = "CHANGES_REQUESTED"mergeableState = "clean"updatedAt >= action.createdAtCurrent classifier path:
positiveOpenSignal = false || true, becausemergeableState === "clean"."improved".CHANGES_REQUESTEDbranch is never reached.This corrupts the recommendation-quality feedback loop:
improvedis counted as positive inrecommendation-quality-report.ts(POSITIVE_STATES)rejectedis counted as negative (NEGATIVE_STATES)Concrete reproduction
Call
classifyRecommendationOutcomewith:targetRepoFullName: "owner/rejected-clean",targetPullNumber: 200,createdAt: "2026-05-01T00:00:00.000Z"state: "open",updatedAt: "2026-05-10T00:00:00.000Z",reviewDecision: "CHANGES_REQUESTED",mergeableState: "clean"evaluatedAt: "2026-06-01T00:00:00.000Z"Expected outcome:
Actual outcome:
Test status
Partially covered, but the conflicting-signal case is missing.
Existing tests prove each individual branch works in isolation:
No fixture combines
reviewDecision: "CHANGES_REQUESTED"withmergeableState: "clean", so the precedence bug is not locked in.Expected behavior
For targeted PR outcomes, negative review decisions should take precedence over mergeability-only positive signals:
"merged""closed"CHANGES_REQUESTEDafter recommendation ->"rejected"APPROVEDafter recommendation ->"improved""improved"Actual behavior
mergeableState === "clean"is folded intopositiveOpenSignaland checked beforeCHANGES_REQUESTED, so a technically merge-clean PR with requested changes is reported as improved.Suggested fix
Prioritize explicit negative review decisions before mergeability-based improvement:
Add fail-on-revert coverage to
test/unit/recommendation-outcomes.test.ts: a targeted open PR with bothreviewDecision: "CHANGES_REQUESTED"andmergeableState: "clean"must classify as"rejected", not"improved".Distinct from prior reports
This is not a duplicate of the earlier changes-requested bug in
gittensory-philluiz-issue3.md.src/signals/local-branch.ts, where stale PR classification runs before blocked PR classification and inflates score-preview scenarios.src/services/recommendation-outcomes.ts, where a targeted PR's feedback outcome is marked positive (improved) before the explicit negative review state (rejected) can be observed.The blast radius is different too: issue 3 affects scenario/open-PR pressure projections; this one affects recommendation-quality history, private summaries, role/report rollups, and the agent feedback loop.