diff --git a/src/scoring/preview.ts b/src/scoring/preview.ts index 27a4c6d74c..a0e8229485 100644 --- a/src/scoring/preview.ts +++ b/src/scoring/preview.ts @@ -303,10 +303,12 @@ function computeScoreCore( const changesRequestedCount = nonNegative(input.changesRequestedCount); const reviewPenaltyMultiplier = clamp(1 - changesRequestedCount * constant(constants, "REVIEW_PENALTY_RATE", 0.15), 0, 1); const openPrCount = nonNegative(input.openPrCount); + // The concurrency allowance is earned from the contributor's established merged-history token + // score; the planned PR's own tokens (totalTokenScore) must not inflate its own open-PR threshold. const openPrThreshold = Math.min( constant(constants, "MAX_OPEN_PR_THRESHOLD", 30), constant(constants, "EXCESSIVE_PR_PENALTY_BASE_THRESHOLD", 2) + - Math.floor((nonNegative(input.existingContributorTokenScore) + totalTokenScore) / constant(constants, "OPEN_PR_THRESHOLD_TOKEN_SCORE", 300)), + Math.floor(nonNegative(input.existingContributorTokenScore) / constant(constants, "OPEN_PR_THRESHOLD_TOKEN_SCORE", 300)), ); const openPrMultiplier = openPrCount <= openPrThreshold ? 1 : 0; const estimatedMergedScore = roundScore(baseScore * labelMultiplier * issueMultiplier * credibilityMultiplier * reviewPenaltyMultiplier * openPrMultiplier); diff --git a/test/unit/scoring.test.ts b/test/unit/scoring.test.ts index 73f20e8d99..e18eabf148 100644 --- a/test/unit/scoring.test.ts +++ b/test/unit/scoring.test.ts @@ -540,6 +540,43 @@ MAX_CODE_DENSITY_MULTIPLIER = 1.15 expect(observedNote).not.toMatch(/6 pending|user-supplied/); }); + it("derives the open-PR threshold from established merged history, not the planned PR's own tokens", () => { + // No merged history, but a large planned PR (totalTokenScore 900) and 3 open PRs. + const noHistory = buildScorePreview({ + repo, + snapshot, + input: { + repoFullName: repo.fullName, + sourceTokenScore: 60, + totalTokenScore: 900, + sourceLines: 50, + openPrCount: 3, + credibility: 1, + existingContributorTokenScore: 0, + }, + }); + // The planned PR's own 900 tokens must NOT inflate its own threshold: base 2 + floor(0/300) = 2. + expect(noHistory.gates.openPrThreshold).toBe(2); + expect(noHistory.scoreEstimate.openPrMultiplier).toBe(0); // 3 > 2 -> open-PR spam gate blocks + + // Established merged-history token score DOES raise the allowance: 2 + floor(900/300) = 5. + const withHistory = buildScorePreview({ + repo, + snapshot, + input: { + repoFullName: repo.fullName, + sourceTokenScore: 60, + totalTokenScore: 900, + sourceLines: 50, + openPrCount: 3, + credibility: 1, + existingContributorTokenScore: 900, + }, + }); + expect(withHistory.gates.openPrThreshold).toBe(5); + expect(withHistory.scoreEstimate.openPrMultiplier).toBe(1); // 3 <= 5 -> passes + }); + it("warns on metadata-only weak previews without using public reward or wallet language", () => { const preview = buildScorePreview({ repo: null,