Summary
buildRepoOutcomePatterns builds its analyzed PR set by iterating only args.pullRequests, and uses args.recentMergedPullRequests purely as an enrichment lookup for PRs that already appear in args.pullRequests:
// src/signals/engine.ts:1780-1783 — recent-merged is only a by-number lookup map
const mergedDetailByNumber = new Map<number, RecentMergedPullRequestRecord>();
for (const record of args.recentMergedPullRequests ?? []) {
if (record.repoFullName.toLowerCase() === repoKey) mergedDetailByNumber.set(record.number, record);
}
...
// src/signals/engine.ts:1802-1805 — analyzed iterates ONLY args.pullRequests
const analyzed: RepoOutcomePullRequest[] = args.pullRequests
.filter((pr) => pr.repoFullName.toLowerCase() === repoKey)
.map((pr) => {
const mergedDetail = mergedDetailByNumber.get(pr.number); // enrich only; never adds merged PRs
...
});
A merged PR that exists only in the recent_merged_pull_requests table (i.e. not also in pull_requests) is never added to analyzed, so it is invisible to totals.merged, outsideContributorMergeRate, the per-dimension merge rates, and every success/risk pattern.
Why merged PRs are absent from args.pullRequests
The production loader reads the two from separate tables:
// src/services/repo-outcome-patterns.ts:74-75
listPullRequests(env, fullName), // -> pull_requests table
listRecentMergedPullRequests(env, fullName), // -> recent_merged_pull_requests table
And the backfill writes merged PRs to the recent-merged table only, while the pull_requests table gets open PRs (and reconciled-closed shells):
- The
open_pull_requests segment fetches state=open and upserts into pull_requests, then markUnseenOpenPullRequestsClosed flips any vanished open PR to state:"closed" with no mergedAt (src/github/backfill.ts:884-897).
- The
recent_merged_pull_requests segment fetches merged PRs and writes them via upsertRecentMergedPullRequest to the separate recent_merged_pull_requests table (src/github/backfill.ts:915-925). The monolithic path does the same split (backfill.ts:1321-1326).
So for any repo populated by backfill (every registered repo), the historical merged PRs live only in recent_merged_pull_requests. listPullRequests (src/db/repositories.ts) returns only the pull_requests rows. Net: args.pullRequests contains open + reconciled-closed (+ a few PRs merged live via webhook while installed) — but not the merged history.
Downstream effect
outsideContributorMergeRate and every outcome dimension are computed from analyzed (which excludes the merged history):
// src/signals/engine.ts:1843
const outsideContributorMergeRate = rate(outsideDecided.filter((pr) => pr.merged).length, outsideDecided.length);
// :1854 — all 7 dimensions iterate only outsideDecided
for (const pr of outsideDecided) { ...path/label/size/linked_issue/test_evidence/author_role/review_churn... }
With the merged PRs missing, outsideContributorMergeRate collapses toward 0, which:
- Suppresses the "Outside contributors merge well here" success pattern (fires only when
>= REPO_OUTCOME_MERGE_WELL_RATE = 0.7, engine.ts:1888).
- Falsely fires the "Outside contributor PRs rarely merge here" / high-closure-risk risk pattern (fires when
<= REPO_OUTCOME_CLOSURE_RISK_RATE = 0.34, engine.ts:1896-1903).
- Compounds via mislabeling: a PR that actually merged but was only ever seen as open and then reconciled to
closed (no mergedAt) is counted as closed_unmerged (engine.ts:1807), pushing the merge rate even lower.
RepoOutcomePatterns feeds buildContributorStrategy, buildRepoFitRecommendation, reward-risk, and the decision pack / maintainer guidance — so a repo that merges outside contributions well is reported as high-closure-risk, steering miners away from exactly the repos where their PRs would land, and corrupting the path/label/size/test-evidence dimension breakdowns (which never see merged history at all).
Failure mode (concrete example)
A registered repo has 5 cached open PRs (in pull_requests) and 200 historically merged outside-contributor PRs (in recent_merged_pull_requests). Of the 5 open, 3 were later reconciled to closed (no mergedAt).
- Current:
analyzed ≈ the 5 pull_requests rows; outsideDecided = the 3 reconciled-closed (counted closed_unmerged); outsideContributorMergeRate = 0/3 = 0 → 0 <= 0.34 → emits "Outside contributor PRs rarely merge here … expect a high closure rate" with confidence medium.
- Correct: the 200 merged PRs are counted →
outsideContributorMergeRate ≈ 0.97 → emits "Outside contributors merge well here".
The reported conclusion is the exact opposite of reality.
Steps to reproduce
- For a repo, populate
pull_requests with a few open/closed PRs and recent_merged_pull_requests with many merged PRs (the normal backfill outcome).
- Call
computeRepoOutcomePatterns(env, repoFullName) (or buildRepoOutcomePatterns directly with pullRequests = open/closed only and recentMergedPullRequests = the merged set).
- Inspect
totals.merged and riskPatterns: totals.merged ≈ 0 and a false "Outside contributor PRs rarely merge here" risk pattern, instead of the merged PRs being counted.
Expected behavior
The analyzed PR set includes the merged PRs from recent_merged_pull_requests, so totals.merged, outsideContributorMergeRate, the dimension merge rates, and the success/risk patterns reflect real merge history.
Actual behavior
analyzed iterates only args.pullRequests; recentMergedPullRequests is used only to enrich already-present PRs, so the merged history is dropped from every count and rate, producing false high-closure-risk patterns and blind dimension breakdowns.
Suggested fix
- Build a unified decided-PR set: normalize each
recent_merged_pull_requests record into a RepoOutcomePullRequest (bucket: "merged", decided: true, merged: true, linked from linkedIssues, labels, filePaths from changedFiles), de-duplicate against args.pullRequests by number (prefer the merged record), and feed the merged set through the same analyzed → decided → dimension-grouping logic.
RecentMergedPullRequestRecord (src/types.ts) has no authorAssociation, so derive maintainerLane / authorRole for merged-only PRs from payload.author_association when present, else default to outside/external (the conservative choice for the outside-contributor merge rate). Note changedFiles carries no per-file additions/deletions, so changedLineCount for merged-only PRs is unavailable (use 0 or size-bucket by file count).
- Fix the reconciliation mislabel so a reconciled-
closed PR that actually merged (present in recent_merged_pull_requests) is counted as merged, not closed_unmerged.
- Add fail-on-revert coverage:
buildRepoOutcomePatterns with pullRequests = a few closed PRs and recentMergedPullRequests = many merged outside-contributor PRs must report a high outsideContributorMergeRate / "merge well" pattern (and totals.merged > 0), not a high-closure-risk pattern. Existing tests only pass merged PRs that also appear in pullRequests, so they never exercise the recent-merged-only path.
Summary
buildRepoOutcomePatternsbuilds its analyzed PR set by iterating onlyargs.pullRequests, and usesargs.recentMergedPullRequestspurely as an enrichment lookup for PRs that already appear inargs.pullRequests:A merged PR that exists only in the
recent_merged_pull_requeststable (i.e. not also inpull_requests) is never added toanalyzed, so it is invisible tototals.merged,outsideContributorMergeRate, the per-dimension merge rates, and every success/risk pattern.Why merged PRs are absent from
args.pullRequestsThe production loader reads the two from separate tables:
And the backfill writes merged PRs to the recent-merged table only, while the
pull_requeststable gets open PRs (and reconciled-closed shells):open_pull_requestssegment fetchesstate=openand upserts intopull_requests, thenmarkUnseenOpenPullRequestsClosedflips any vanished open PR tostate:"closed"with nomergedAt(src/github/backfill.ts:884-897).recent_merged_pull_requestssegment fetches merged PRs and writes them viaupsertRecentMergedPullRequestto the separaterecent_merged_pull_requeststable (src/github/backfill.ts:915-925). The monolithic path does the same split (backfill.ts:1321-1326).So for any repo populated by backfill (every registered repo), the historical merged PRs live only in
recent_merged_pull_requests.listPullRequests(src/db/repositories.ts) returns only thepull_requestsrows. Net:args.pullRequestscontains open + reconciled-closed (+ a few PRs merged live via webhook while installed) — but not the merged history.Downstream effect
outsideContributorMergeRateand every outcome dimension are computed fromanalyzed(which excludes the merged history):With the merged PRs missing,
outsideContributorMergeRatecollapses toward 0, which:>= REPO_OUTCOME_MERGE_WELL_RATE= 0.7,engine.ts:1888).<= REPO_OUTCOME_CLOSURE_RISK_RATE= 0.34,engine.ts:1896-1903).closed(nomergedAt) is counted asclosed_unmerged(engine.ts:1807), pushing the merge rate even lower.RepoOutcomePatternsfeedsbuildContributorStrategy,buildRepoFitRecommendation, reward-risk, and the decision pack / maintainer guidance — so a repo that merges outside contributions well is reported as high-closure-risk, steering miners away from exactly the repos where their PRs would land, and corrupting the path/label/size/test-evidence dimension breakdowns (which never see merged history at all).Failure mode (concrete example)
A registered repo has 5 cached open PRs (in
pull_requests) and 200 historically merged outside-contributor PRs (inrecent_merged_pull_requests). Of the 5 open, 3 were later reconciled toclosed(nomergedAt).analyzed≈ the 5pull_requestsrows;outsideDecided= the 3 reconciled-closed (countedclosed_unmerged);outsideContributorMergeRate = 0/3 = 0→0 <= 0.34→ emits "Outside contributor PRs rarely merge here … expect a high closure rate" with confidencemedium.outsideContributorMergeRate≈ 0.97 → emits "Outside contributors merge well here".The reported conclusion is the exact opposite of reality.
Steps to reproduce
pull_requestswith a few open/closed PRs andrecent_merged_pull_requestswith many merged PRs (the normal backfill outcome).computeRepoOutcomePatterns(env, repoFullName)(orbuildRepoOutcomePatternsdirectly withpullRequests= open/closed only andrecentMergedPullRequests= the merged set).totals.mergedandriskPatterns:totals.merged≈ 0 and a false"Outside contributor PRs rarely merge here"risk pattern, instead of the merged PRs being counted.Expected behavior
The analyzed PR set includes the merged PRs from
recent_merged_pull_requests, sototals.merged,outsideContributorMergeRate, the dimension merge rates, and the success/risk patterns reflect real merge history.Actual behavior
analyzediterates onlyargs.pullRequests;recentMergedPullRequestsis used only to enrich already-present PRs, so the merged history is dropped from every count and rate, producing false high-closure-risk patterns and blind dimension breakdowns.Suggested fix
recent_merged_pull_requestsrecord into aRepoOutcomePullRequest(bucket: "merged",decided: true,merged: true,linkedfromlinkedIssues,labels,filePathsfromchangedFiles), de-duplicate againstargs.pullRequestsbynumber(prefer the merged record), and feed the merged set through the sameanalyzed→decided→ dimension-grouping logic.RecentMergedPullRequestRecord(src/types.ts) has noauthorAssociation, so derivemaintainerLane/authorRolefor merged-only PRs frompayload.author_associationwhen present, else default to outside/external (the conservative choice for the outside-contributor merge rate). NotechangedFilescarries no per-file additions/deletions, sochangedLineCountfor merged-only PRs is unavailable (use 0 or size-bucket by file count).closedPR that actually merged (present inrecent_merged_pull_requests) is counted asmerged, notclosed_unmerged.buildRepoOutcomePatternswithpullRequests= a few closed PRs andrecentMergedPullRequests= many merged outside-contributor PRs must report a highoutsideContributorMergeRate/ "merge well" pattern (andtotals.merged> 0), not a high-closure-risk pattern. Existing tests only pass merged PRs that also appear inpullRequests, so they never exercise the recent-merged-only path.