From ff6b047191f3937bfdb083002072eea14b4e17d2 Mon Sep 17 00:00:00 2001 From: bohdansolovie Date: Thu, 25 Jun 2026 01:21:13 +0200 Subject: [PATCH 1/3] feat(scoring): wire contributor validity floors into score preview (#808) Mirror upstream merged-PR and issue-discovery eligibility gates in preview, local branch wiring, scenario projections, and API/OpenAPI schemas. Co-authored-by: Cursor --- apps/gittensory-ui/public/openapi.json | 363 ++++++++++++++++++++++++- src/api/routes.ts | 3 + src/openapi/schemas.ts | 25 +- src/scenarios/scenario-summary.ts | 2 + src/scoring/preview.ts | 149 +++++++++- src/signals/local-branch.ts | 5 +- test/unit/scoring.test.ts | 195 +++++++++++++ 7 files changed, 721 insertions(+), 21 deletions(-) diff --git a/apps/gittensory-ui/public/openapi.json b/apps/gittensory-ui/public/openapi.json index b879fdc255..f594f5b93d 100644 --- a/apps/gittensory-ui/public/openapi.json +++ b/apps/gittensory-ui/public/openapi.json @@ -4263,6 +4263,15 @@ }, "timeDecayMultiplier": { "type": "number" + }, + "openIssueMultiplier": { + "type": "number" + }, + "mergedHistoryMultiplier": { + "type": "number" + }, + "issueDiscoveryHistoryMultiplier": { + "type": "number" } }, "required": [ @@ -4274,6 +4283,9 @@ "credibilityMultiplier", "reviewPenaltyMultiplier", "openPrMultiplier", + "openIssueMultiplier", + "mergedHistoryMultiplier", + "issueDiscoveryHistoryMultiplier", "timeDecayMultiplier", "estimatedMergedScore", "pendingSaturationScore" @@ -4302,6 +4314,30 @@ }, "reviewCollateralMultiplier": { "type": "number" + }, + "openIssueThreshold": { + "type": "number" + }, + "openIssueCount": { + "type": "number" + }, + "mergedPrFloor": { + "type": "number" + }, + "mergedPullRequests": { + "type": "number" + }, + "validSolvedIssuesFloor": { + "type": "number" + }, + "validSolvedIssues": { + "type": "number" + }, + "issueCredibilityFloor": { + "type": "number" + }, + "issueCredibility": { + "type": "number" } }, "required": [ @@ -4311,7 +4347,12 @@ "collateralFraction", "reviewCollateralMultiplier", "credibilityFloor", - "credibilityObserved" + "credibilityObserved", + "openIssueThreshold", + "openIssueCount", + "mergedPrFloor", + "validSolvedIssuesFloor", + "issueCredibilityFloor" ] }, "effectiveEstimatedScore": { @@ -4332,13 +4373,18 @@ "inactive_allocation", "base_token_gate", "open_pr_threshold", + "open_issue_threshold", + "merged_pr_history_floor", + "issue_discovery_validity_floor", "credibility_floor", "review_penalty", "metadata_only", "linked_issue_invalid", "linked_issue_unvalidated", "branch_ineligible", - "branch_eligibility_missing" + "branch_eligibility_missing", + "duplicate_risk", + "stale_work" ] }, "severity": { @@ -4519,6 +4565,15 @@ }, "timeDecayMultiplier": { "type": "number" + }, + "openIssueMultiplier": { + "type": "number" + }, + "mergedHistoryMultiplier": { + "type": "number" + }, + "issueDiscoveryHistoryMultiplier": { + "type": "number" } }, "required": [ @@ -4530,6 +4585,9 @@ "credibilityMultiplier", "reviewPenaltyMultiplier", "openPrMultiplier", + "openIssueMultiplier", + "mergedHistoryMultiplier", + "issueDiscoveryHistoryMultiplier", "timeDecayMultiplier", "estimatedMergedScore", "pendingSaturationScore" @@ -4558,6 +4616,30 @@ }, "reviewCollateralMultiplier": { "type": "number" + }, + "openIssueThreshold": { + "type": "number" + }, + "openIssueCount": { + "type": "number" + }, + "mergedPrFloor": { + "type": "number" + }, + "mergedPullRequests": { + "type": "number" + }, + "validSolvedIssuesFloor": { + "type": "number" + }, + "validSolvedIssues": { + "type": "number" + }, + "issueCredibilityFloor": { + "type": "number" + }, + "issueCredibility": { + "type": "number" } }, "required": [ @@ -4567,7 +4649,12 @@ "collateralFraction", "reviewCollateralMultiplier", "credibilityFloor", - "credibilityObserved" + "credibilityObserved", + "openIssueThreshold", + "openIssueCount", + "mergedPrFloor", + "validSolvedIssuesFloor", + "issueCredibilityFloor" ] }, "effectiveEstimatedScore": { @@ -4588,13 +4675,18 @@ "inactive_allocation", "base_token_gate", "open_pr_threshold", + "open_issue_threshold", + "merged_pr_history_floor", + "issue_discovery_validity_floor", "credibility_floor", "review_penalty", "metadata_only", "linked_issue_invalid", "linked_issue_unvalidated", "branch_ineligible", - "branch_eligibility_missing" + "branch_eligibility_missing", + "duplicate_risk", + "stale_work" ] }, "severity": { @@ -4775,6 +4867,15 @@ }, "timeDecayMultiplier": { "type": "number" + }, + "openIssueMultiplier": { + "type": "number" + }, + "mergedHistoryMultiplier": { + "type": "number" + }, + "issueDiscoveryHistoryMultiplier": { + "type": "number" } }, "required": [ @@ -4786,6 +4887,9 @@ "credibilityMultiplier", "reviewPenaltyMultiplier", "openPrMultiplier", + "openIssueMultiplier", + "mergedHistoryMultiplier", + "issueDiscoveryHistoryMultiplier", "timeDecayMultiplier", "estimatedMergedScore", "pendingSaturationScore" @@ -4814,6 +4918,30 @@ }, "reviewCollateralMultiplier": { "type": "number" + }, + "openIssueThreshold": { + "type": "number" + }, + "openIssueCount": { + "type": "number" + }, + "mergedPrFloor": { + "type": "number" + }, + "mergedPullRequests": { + "type": "number" + }, + "validSolvedIssuesFloor": { + "type": "number" + }, + "validSolvedIssues": { + "type": "number" + }, + "issueCredibilityFloor": { + "type": "number" + }, + "issueCredibility": { + "type": "number" } }, "required": [ @@ -4823,7 +4951,12 @@ "collateralFraction", "reviewCollateralMultiplier", "credibilityFloor", - "credibilityObserved" + "credibilityObserved", + "openIssueThreshold", + "openIssueCount", + "mergedPrFloor", + "validSolvedIssuesFloor", + "issueCredibilityFloor" ] }, "effectiveEstimatedScore": { @@ -4844,13 +4977,18 @@ "inactive_allocation", "base_token_gate", "open_pr_threshold", + "open_issue_threshold", + "merged_pr_history_floor", + "issue_discovery_validity_floor", "credibility_floor", "review_penalty", "metadata_only", "linked_issue_invalid", "linked_issue_unvalidated", "branch_ineligible", - "branch_eligibility_missing" + "branch_eligibility_missing", + "duplicate_risk", + "stale_work" ] }, "severity": { @@ -5031,6 +5169,15 @@ }, "timeDecayMultiplier": { "type": "number" + }, + "openIssueMultiplier": { + "type": "number" + }, + "mergedHistoryMultiplier": { + "type": "number" + }, + "issueDiscoveryHistoryMultiplier": { + "type": "number" } }, "required": [ @@ -5042,6 +5189,9 @@ "credibilityMultiplier", "reviewPenaltyMultiplier", "openPrMultiplier", + "openIssueMultiplier", + "mergedHistoryMultiplier", + "issueDiscoveryHistoryMultiplier", "timeDecayMultiplier", "estimatedMergedScore", "pendingSaturationScore" @@ -5070,6 +5220,30 @@ }, "reviewCollateralMultiplier": { "type": "number" + }, + "openIssueThreshold": { + "type": "number" + }, + "openIssueCount": { + "type": "number" + }, + "mergedPrFloor": { + "type": "number" + }, + "mergedPullRequests": { + "type": "number" + }, + "validSolvedIssuesFloor": { + "type": "number" + }, + "validSolvedIssues": { + "type": "number" + }, + "issueCredibilityFloor": { + "type": "number" + }, + "issueCredibility": { + "type": "number" } }, "required": [ @@ -5079,7 +5253,12 @@ "collateralFraction", "reviewCollateralMultiplier", "credibilityFloor", - "credibilityObserved" + "credibilityObserved", + "openIssueThreshold", + "openIssueCount", + "mergedPrFloor", + "validSolvedIssuesFloor", + "issueCredibilityFloor" ] }, "effectiveEstimatedScore": { @@ -5100,13 +5279,18 @@ "inactive_allocation", "base_token_gate", "open_pr_threshold", + "open_issue_threshold", + "merged_pr_history_floor", + "issue_discovery_validity_floor", "credibility_floor", "review_penalty", "metadata_only", "linked_issue_invalid", "linked_issue_unvalidated", "branch_ineligible", - "branch_eligibility_missing" + "branch_eligibility_missing", + "duplicate_risk", + "stale_work" ] }, "severity": { @@ -5287,6 +5471,15 @@ }, "timeDecayMultiplier": { "type": "number" + }, + "openIssueMultiplier": { + "type": "number" + }, + "mergedHistoryMultiplier": { + "type": "number" + }, + "issueDiscoveryHistoryMultiplier": { + "type": "number" } }, "required": [ @@ -5298,6 +5491,9 @@ "credibilityMultiplier", "reviewPenaltyMultiplier", "openPrMultiplier", + "openIssueMultiplier", + "mergedHistoryMultiplier", + "issueDiscoveryHistoryMultiplier", "timeDecayMultiplier", "estimatedMergedScore", "pendingSaturationScore" @@ -5326,6 +5522,30 @@ }, "reviewCollateralMultiplier": { "type": "number" + }, + "openIssueThreshold": { + "type": "number" + }, + "openIssueCount": { + "type": "number" + }, + "mergedPrFloor": { + "type": "number" + }, + "mergedPullRequests": { + "type": "number" + }, + "validSolvedIssuesFloor": { + "type": "number" + }, + "validSolvedIssues": { + "type": "number" + }, + "issueCredibilityFloor": { + "type": "number" + }, + "issueCredibility": { + "type": "number" } }, "required": [ @@ -5335,7 +5555,12 @@ "collateralFraction", "reviewCollateralMultiplier", "credibilityFloor", - "credibilityObserved" + "credibilityObserved", + "openIssueThreshold", + "openIssueCount", + "mergedPrFloor", + "validSolvedIssuesFloor", + "issueCredibilityFloor" ] }, "effectiveEstimatedScore": { @@ -5356,13 +5581,18 @@ "inactive_allocation", "base_token_gate", "open_pr_threshold", + "open_issue_threshold", + "merged_pr_history_floor", + "issue_discovery_validity_floor", "credibility_floor", "review_penalty", "metadata_only", "linked_issue_invalid", "linked_issue_unvalidated", "branch_ineligible", - "branch_eligibility_missing" + "branch_eligibility_missing", + "duplicate_risk", + "stale_work" ] }, "severity": { @@ -5487,6 +5717,9 @@ "type": "string", "enum": [ "open_pr_threshold", + "open_issue_threshold", + "merged_pr_history_floor", + "issue_discovery_validity_floor", "credibility_floor", "linked_issue_multiplier" ] @@ -5521,13 +5754,18 @@ "inactive_allocation", "base_token_gate", "open_pr_threshold", + "open_issue_threshold", + "merged_pr_history_floor", + "issue_discovery_validity_floor", "credibility_floor", "review_penalty", "metadata_only", "linked_issue_invalid", "linked_issue_unvalidated", "branch_ineligible", - "branch_eligibility_missing" + "branch_eligibility_missing", + "duplicate_risk", + "stale_work" ] }, "severity": { @@ -6114,6 +6352,15 @@ }, "timeDecayMultiplier": { "type": "number" + }, + "openIssueMultiplier": { + "type": "number" + }, + "mergedHistoryMultiplier": { + "type": "number" + }, + "issueDiscoveryHistoryMultiplier": { + "type": "number" } }, "required": [ @@ -6125,6 +6372,9 @@ "credibilityMultiplier", "reviewPenaltyMultiplier", "openPrMultiplier", + "openIssueMultiplier", + "mergedHistoryMultiplier", + "issueDiscoveryHistoryMultiplier", "timeDecayMultiplier", "estimatedMergedScore", "pendingSaturationScore" @@ -6230,6 +6480,30 @@ }, "reviewCollateralMultiplier": { "type": "number" + }, + "openIssueThreshold": { + "type": "number" + }, + "openIssueCount": { + "type": "number" + }, + "mergedPrFloor": { + "type": "number" + }, + "mergedPullRequests": { + "type": "number" + }, + "validSolvedIssuesFloor": { + "type": "number" + }, + "validSolvedIssues": { + "type": "number" + }, + "issueCredibilityFloor": { + "type": "number" + }, + "issueCredibility": { + "type": "number" } }, "required": [ @@ -6239,7 +6513,12 @@ "collateralFraction", "reviewCollateralMultiplier", "credibilityFloor", - "credibilityObserved" + "credibilityObserved", + "openIssueThreshold", + "openIssueCount", + "mergedPrFloor", + "validSolvedIssuesFloor", + "issueCredibilityFloor" ] }, "branchEligibility": { @@ -6317,13 +6596,18 @@ "inactive_allocation", "base_token_gate", "open_pr_threshold", + "open_issue_threshold", + "merged_pr_history_floor", + "issue_discovery_validity_floor", "credibility_floor", "review_penalty", "metadata_only", "linked_issue_invalid", "linked_issue_unvalidated", "branch_ineligible", - "branch_eligibility_missing" + "branch_eligibility_missing", + "duplicate_risk", + "stale_work" ] }, "severity": { @@ -6354,6 +6638,9 @@ "type": "string", "enum": [ "open_pr_threshold", + "open_issue_threshold", + "merged_pr_history_floor", + "issue_discovery_validity_floor", "credibility_floor", "linked_issue_multiplier" ] @@ -6443,6 +6730,15 @@ }, "timeDecayMultiplier": { "type": "number" + }, + "openIssueMultiplier": { + "type": "number" + }, + "mergedHistoryMultiplier": { + "type": "number" + }, + "issueDiscoveryHistoryMultiplier": { + "type": "number" } }, "required": [ @@ -6454,6 +6750,9 @@ "credibilityMultiplier", "reviewPenaltyMultiplier", "openPrMultiplier", + "openIssueMultiplier", + "mergedHistoryMultiplier", + "issueDiscoveryHistoryMultiplier", "timeDecayMultiplier", "estimatedMergedScore", "pendingSaturationScore" @@ -6482,6 +6781,30 @@ }, "reviewCollateralMultiplier": { "type": "number" + }, + "openIssueThreshold": { + "type": "number" + }, + "openIssueCount": { + "type": "number" + }, + "mergedPrFloor": { + "type": "number" + }, + "mergedPullRequests": { + "type": "number" + }, + "validSolvedIssuesFloor": { + "type": "number" + }, + "validSolvedIssues": { + "type": "number" + }, + "issueCredibilityFloor": { + "type": "number" + }, + "issueCredibility": { + "type": "number" } }, "required": [ @@ -6491,7 +6814,12 @@ "collateralFraction", "reviewCollateralMultiplier", "credibilityFloor", - "credibilityObserved" + "credibilityObserved", + "openIssueThreshold", + "openIssueCount", + "mergedPrFloor", + "validSolvedIssuesFloor", + "issueCredibilityFloor" ] }, "effectiveEstimatedScore": { @@ -6512,13 +6840,18 @@ "inactive_allocation", "base_token_gate", "open_pr_threshold", + "open_issue_threshold", + "merged_pr_history_floor", + "issue_discovery_validity_floor", "credibility_floor", "review_penalty", "metadata_only", "linked_issue_invalid", "linked_issue_unvalidated", "branch_ineligible", - "branch_eligibility_missing" + "branch_eligibility_missing", + "duplicate_risk", + "stale_work" ] }, "severity": { diff --git a/src/api/routes.ts b/src/api/routes.ts index f20d366a59..78fa97e63e 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -551,6 +551,9 @@ const scorePreviewSchema = z.object({ existingContributorTokenScore: z.number().min(0).optional(), prAgeHours: z.number().min(0).optional(), openPrCount: z.number().int().min(0).optional(), + mergedPullRequests: z.number().int().min(0).optional(), + validSolvedIssues: z.number().int().min(0).optional(), + issueCredibility: z.number().min(0).max(1).optional(), credibility: z.number().min(0).max(1).optional(), changesRequestedCount: z.number().int().min(0).optional(), duplicateRiskCount: z.number().int().min(0).optional(), diff --git a/src/openapi/schemas.ts b/src/openapi/schemas.ts index 5ac05c0465..d9879aa3ec 100644 --- a/src/openapi/schemas.ts +++ b/src/openapi/schemas.ts @@ -1254,6 +1254,9 @@ const ScoreEstimateSchema = z.object({ credibilityMultiplier: z.number(), reviewPenaltyMultiplier: z.number(), openPrMultiplier: z.number(), + openIssueMultiplier: z.number(), + mergedHistoryMultiplier: z.number(), + issueDiscoveryHistoryMultiplier: z.number(), timeDecayMultiplier: z.number(), estimatedMergedScore: z.number(), pendingSaturationScore: z.number(), @@ -1267,6 +1270,14 @@ const ScoreGatesSchema = z.object({ reviewCollateralMultiplier: z.number(), credibilityFloor: z.number(), credibilityObserved: z.number(), + openIssueThreshold: z.number(), + openIssueCount: z.number(), + mergedPrFloor: z.number(), + mergedPullRequests: z.number().optional(), + validSolvedIssuesFloor: z.number(), + validSolvedIssues: z.number().optional(), + issueCredibilityFloor: z.number(), + issueCredibility: z.number().optional(), }); const BranchEligibilitySchema = z.object({ @@ -1286,6 +1297,9 @@ const ScoreGateBlockerSchema = z.object({ "inactive_allocation", "base_token_gate", "open_pr_threshold", + "open_issue_threshold", + "merged_pr_history_floor", + "issue_discovery_validity_floor", "credibility_floor", "review_penalty", "metadata_only", @@ -1293,13 +1307,22 @@ const ScoreGateBlockerSchema = z.object({ "linked_issue_unvalidated", "branch_ineligible", "branch_eligibility_missing", + "duplicate_risk", + "stale_work", ]), severity: z.enum(["blocker", "reducer", "context"]), detail: z.string(), }); const ScoreGateDeltaSchema = z.object({ - gate: z.enum(["open_pr_threshold", "credibility_floor", "linked_issue_multiplier"]), + gate: z.enum([ + "open_pr_threshold", + "open_issue_threshold", + "merged_pr_history_floor", + "issue_discovery_validity_floor", + "credibility_floor", + "linked_issue_multiplier", + ]), current: z.string(), projected: z.string(), explanation: z.string(), diff --git a/src/scenarios/scenario-summary.ts b/src/scenarios/scenario-summary.ts index 3e3408ccf6..2ccdffab69 100644 --- a/src/scenarios/scenario-summary.ts +++ b/src/scenarios/scenario-summary.ts @@ -79,6 +79,8 @@ const PUBLIC_BLOCKER_TEXT: Partial> = { base_token_gate: "The change size may be too small to meet the contribution threshold.", open_pr_threshold: "Too many concurrent open PRs exist; landing or closing some would help.", open_issue_threshold: "Too many open issues exist; closing excess issues would help.", + merged_pr_history_floor: "Merged PR history on this repo is below the upstream eligibility floor.", + issue_discovery_validity_floor: "Valid solved-issue history or issue credibility is below the upstream issue-discovery floor.", credibility_floor: "Contributor credibility evidence is below the expected floor.", review_penalty: "Review churn history may reduce the contribution quality signal.", metadata_only: "Only metadata signals are available; detailed analysis requires full context.", diff --git a/src/scoring/preview.ts b/src/scoring/preview.ts index 7e495c32f2..96d8a36799 100644 --- a/src/scoring/preview.ts +++ b/src/scoring/preview.ts @@ -21,6 +21,12 @@ export type ScorePreviewInput = { openPrCount?: number | undefined; /** Contributor's current open-issue count for the repo, used for the open-issue spam gate (#808). */ openIssueCount?: number | undefined; + /** Repo-level merged PR count for upstream contributor-history eligibility (#808). */ + mergedPullRequests?: number | undefined; + /** Count of valid solved issues for upstream issue-discovery eligibility (#808). */ + validSolvedIssues?: number | undefined; + /** Issue-discovery credibility for upstream issue-discovery eligibility (#808). */ + issueCredibility?: number | undefined; credibility?: number | undefined; changesRequestedCount?: number | undefined; fixedBaseScore?: number | undefined; @@ -107,6 +113,8 @@ export type ScoreGateBlocker = { | "base_token_gate" | "open_pr_threshold" | "open_issue_threshold" + | "merged_pr_history_floor" + | "issue_discovery_validity_floor" | "credibility_floor" | "review_penalty" | "metadata_only" @@ -121,7 +129,13 @@ export type ScoreGateBlocker = { }; export type ScoreGateDelta = { - gate: "open_pr_threshold" | "open_issue_threshold" | "credibility_floor" | "linked_issue_multiplier"; + gate: + | "open_pr_threshold" + | "open_issue_threshold" + | "merged_pr_history_floor" + | "issue_discovery_validity_floor" + | "credibility_floor" + | "linked_issue_multiplier"; current: string; projected: string; explanation: string; @@ -164,6 +178,10 @@ export type ScorePreviewResult = { reviewPenaltyMultiplier: number; openPrMultiplier: number; openIssueMultiplier: number; + /** Upstream merged-PR history floor (#808). 0 when below MIN_VALID_MERGED_PRS; 1 when unknown or eligible. */ + mergedHistoryMultiplier: number; + /** Upstream issue-discovery validity floor (#808). 0 when below MIN_VALID_SOLVED_ISSUES or MIN_ISSUE_CREDIBILITY. */ + issueDiscoveryHistoryMultiplier: number; /** Upstream sigmoid time-decay multiplier (#703). 1 = no decay (fresh PR, or feature off). */ timeDecayMultiplier: number; estimatedMergedScore: number; @@ -182,6 +200,15 @@ export type ScorePreviewResult = { credibilityObserved: number; openIssueThreshold: number; openIssueCount: number; + mergedPrFloor: number; + /** Observed merged PR count when supplied or inferred from contributor evidence; absent when unknown. */ + mergedPullRequests?: number | undefined; + validSolvedIssuesFloor: number; + /** Observed valid solved-issue count when supplied; absent when unknown. */ + validSolvedIssues?: number | undefined; + issueCredibilityFloor: number; + /** Observed issue-discovery credibility when supplied; absent when unknown. */ + issueCredibility?: number | undefined; }; branchEligibility: BranchEligibilityResult; effectiveEstimatedScore: number; @@ -217,6 +244,10 @@ export function buildScorePreview(args: { ...(!current.gates.baseTokenGatePassed ? ["Increase meaningful source change size or scope clarity before relying on this preview."] : []), ...(current.scoreEstimate.openPrMultiplier === 0 ? ["Land or close existing open PRs before opening more concurrent work."] : []), ...(current.scoreEstimate.openIssueMultiplier === 0 ? ["Close excess open issues to stay within the open-issue spam threshold."] : []), + ...(current.scoreEstimate.mergedHistoryMultiplier === 0 ? ["Build merged PR history on this repo before relying on this preview; upstream requires a minimum merged count."] : []), + ...(current.scoreEstimate.issueDiscoveryHistoryMultiplier === 0 + ? ["Build valid solved-issue history and issue credibility before relying on issue-discovery scoring on this repo."] + : []), ...(current.scoreEstimate.credibilityMultiplier < 1 ? ["Build or wait for contributor credibility evidence before relying on this preview."] : []), ...(current.scoreEstimate.reviewPenaltyMultiplier < 1 ? ["Reduce review churn with tighter tests and clearer evidence."] : []), ...(current.scoreEstimate.labelMultiplier <= 1 && Object.keys(args.repo?.registryConfig?.labelMultipliers ?? {}).length > 0 @@ -365,13 +396,38 @@ function computeScoreCore( Math.floor(nonNegative(input.existingContributorTokenScore) / constant(constants, "OPEN_ISSUE_SPAM_TOKEN_SCORE_PER_SLOT")), ); const openIssueMultiplier = openIssueCount <= openIssueThreshold ? 1 : 0; + const mergedPrFloor = constant(constants, "MIN_VALID_MERGED_PRS"); + const mergedPullRequestsObserved = resolveMergedPullRequests(input, contributorEvidence); + const mergedHistoryMultiplier = + mergedPullRequestsObserved === undefined ? 1 : mergedPullRequestsObserved >= mergedPrFloor ? 1 : 0; + const validSolvedIssuesFloor = constant(constants, "MIN_VALID_SOLVED_ISSUES"); + const issueCredibilityFloor = constant(constants, "MIN_ISSUE_CREDIBILITY"); + const validSolvedIssuesObserved = input.validSolvedIssues !== undefined ? nonNegative(input.validSolvedIssues) : undefined; + const issueCredibilityObserved = input.issueCredibility !== undefined ? clamp(input.issueCredibility, 0, 1) : undefined; + const issueDiscoveryRelevant = issueDiscoveryShare > 0 || (input.linkedIssueMode ?? "none") !== "none"; + const issueDiscoveryHistoryKnown = validSolvedIssuesObserved !== undefined && issueCredibilityObserved !== undefined; + const issueDiscoveryHistoryMultiplier = + !issueDiscoveryRelevant || !issueDiscoveryHistoryKnown + ? 1 + : validSolvedIssuesObserved >= validSolvedIssuesFloor && issueCredibilityObserved >= issueCredibilityFloor + ? 1 + : 0; // Upstream time-decay (#703): mirrors upstream's `scored.time_decay_multiplier` applied to a PR's score. // Opt-in + env-gated (default off). A fresh PR (prAgeHours below the grace period) yields 1.0, so a normal // new-PR preview is unchanged even when enabled — only an aged-PR projection decays. // Per-repo curve (#703): the repo's registry `scoring.time_decay` overrides overlay the snapshot defaults. const timeDecayMultiplier = input.applyTimeDecay ? calculateTimeDecay(nonNegative(input.prAgeHours), constants, config?.timeDecay) : 1; const estimatedMergedScore = roundScore( - baseScore * labelMultiplier * issueMultiplier * credibilityMultiplier * reviewPenaltyMultiplier * openPrMultiplier * openIssueMultiplier * timeDecayMultiplier, + baseScore * + labelMultiplier * + issueMultiplier * + credibilityMultiplier * + reviewPenaltyMultiplier * + openPrMultiplier * + openIssueMultiplier * + mergedHistoryMultiplier * + issueDiscoveryHistoryMultiplier * + timeDecayMultiplier, ); const pendingSaturationScore = roundScore(saturationBaseScore); return { @@ -393,6 +449,8 @@ function computeScoreCore( reviewPenaltyMultiplier: roundScore(reviewPenaltyMultiplier), openPrMultiplier, openIssueMultiplier, + mergedHistoryMultiplier, + issueDiscoveryHistoryMultiplier, timeDecayMultiplier: roundScore(timeDecayMultiplier), estimatedMergedScore, pendingSaturationScore, @@ -408,6 +466,12 @@ function computeScoreCore( credibilityObserved, openIssueThreshold, openIssueCount, + mergedPrFloor, + ...(mergedPullRequestsObserved !== undefined ? { mergedPullRequests: mergedPullRequestsObserved } : {}), + validSolvedIssuesFloor, + ...(validSolvedIssuesObserved !== undefined ? { validSolvedIssues: validSolvedIssuesObserved } : {}), + issueCredibilityFloor, + ...(issueCredibilityObserved !== undefined ? { issueCredibility: issueCredibilityObserved } : {}), }, }; } @@ -454,12 +518,25 @@ function buildScenarioPreviews( const cleanGatesInput = { ...input, openPrCount: Math.min(current.gates.openPrCount, current.gates.openPrThreshold), + openIssueCount: Math.min(current.gates.openIssueCount, current.gates.openIssueThreshold), credibility: Math.max(current.gates.credibilityObserved, current.gates.credibilityFloor), + ...(current.gates.mergedPullRequests !== undefined + ? { mergedPullRequests: Math.max(current.gates.mergedPullRequests, current.gates.mergedPrFloor) } + : {}), + ...(current.gates.validSolvedIssues !== undefined + ? { validSolvedIssues: Math.max(current.gates.validSolvedIssues, current.gates.validSolvedIssuesFloor) } + : {}), + ...(current.gates.issueCredibility !== undefined + ? { issueCredibility: Math.max(current.gates.issueCredibility, current.gates.issueCredibilityFloor) } + : {}), }; const afterPendingInput = { ...input, openPrCount: expectedOpenPrCountAfterMerge, credibility: projectedCredibility, + ...(current.gates.mergedPullRequests !== undefined + ? { mergedPullRequests: nonNegative(current.gates.mergedPullRequests) + mergeReadyPending } + : {}), }; const linkedIssueInput = withValidatedLinkedIssueScenario(input); const bestReasonableInput = { @@ -472,11 +549,25 @@ function buildScenarioPreviews( // "best reasonable case" can clear the open-issue gate just like it clears open-PR pressure. openIssueCount: Math.min(current.gates.openIssueCount, current.gates.openIssueThreshold), credibility: Math.max(projectedCredibility, observedApprovalCredibility, current.gates.credibilityFloor), + ...(current.gates.mergedPullRequests !== undefined + ? { + mergedPullRequests: Math.max( + nonNegative(current.gates.mergedPullRequests) + mergeReadyPending, + current.gates.mergedPrFloor, + ), + } + : {}), + ...(current.gates.validSolvedIssues !== undefined + ? { validSolvedIssues: Math.max(current.gates.validSolvedIssues, current.gates.validSolvedIssuesFloor) } + : {}), + ...(current.gates.issueCredibility !== undefined + ? { issueCredibility: Math.max(current.gates.issueCredibility, current.gates.issueCredibilityFloor) } + : {}), }; return [ scenario("current", "current_data", input, current, ["Current cached/account state and supplied local diff metadata."], repo), scenario("cleanGates", "gittensory_projection", cleanGatesInput, computeScoreCore(cleanGatesInput, repo, snapshot, contributorEvidence), [ - "Open PR and credibility gates are projected as cleared; branch metadata is otherwise unchanged.", + "Open PR, open-issue, credibility, and contributor-history gates are projected as cleared; branch metadata is otherwise unchanged.", ], repo), scenario( "afterPendingMerges", @@ -535,7 +626,7 @@ function buildScenarioPreviews( : "Linked issue mode was already supplied; this scenario projects solved-by-PR validation where needed.", ], repo), scenario("bestReasonableCase", "gittensory_projection", bestReasonableInput, computeScoreCore(bestReasonableInput, repo, snapshot, contributorEvidence), [ - "Combines plausible near-term gate cleanup: open PR pressure at threshold or below, open-issue spam pressure at threshold or below, credibility at floor or above, and linked-issue context where applicable.", + "Combines plausible near-term gate cleanup: open PR pressure at threshold or below, open-issue spam pressure at threshold or below, credibility at floor or above, contributor merged-history and issue-discovery validity at floor or above, and linked-issue context where applicable.", ...(input.scenarioNotes ?? []), ...observedScenarioNotes(input), ], repo), @@ -627,6 +718,24 @@ function blockedByFor(input: ScorePreviewInput, repo: RepositoryRecord | null, c }, ] : []), + ...(core.scoreEstimate.mergedHistoryMultiplier === 0 + ? [ + { + code: "merged_pr_history_floor" as const, + severity: "blocker" as const, + detail: `Merged PR count ${core.gates.mergedPullRequests ?? 0} is below upstream floor ${core.gates.mergedPrFloor}.`, + }, + ] + : []), + ...(core.scoreEstimate.issueDiscoveryHistoryMultiplier === 0 + ? [ + { + code: "issue_discovery_validity_floor" as const, + severity: "blocker" as const, + detail: `Issue-discovery history (${core.gates.validSolvedIssues ?? 0} valid solved, credibility ${roundScore(core.gates.issueCredibility ?? 0)}) is below upstream floors (${core.gates.validSolvedIssuesFloor} valid solved, ${core.gates.issueCredibilityFloor} credibility).`, + }, + ] + : []), ...(core.gates.credibilityObserved < core.gates.credibilityFloor ? [ { @@ -709,6 +818,29 @@ function buildGateDeltas(current: ScoreCore, scenarios: ScoreScenarioPreview[]): }, ] : []), + ...(current.scoreEstimate.mergedHistoryMultiplier !== best.scoreEstimate.mergedHistoryMultiplier || + current.gates.mergedPullRequests !== best.gates.mergedPullRequests + ? [ + { + gate: "merged_pr_history_floor" as const, + current: `${current.gates.mergedPullRequests ?? "unknown"}/${current.gates.mergedPrFloor} merged PRs, multiplier ${current.scoreEstimate.mergedHistoryMultiplier}`, + projected: `${best.gates.mergedPullRequests ?? "unknown"}/${best.gates.mergedPrFloor} merged PRs, multiplier ${best.scoreEstimate.mergedHistoryMultiplier}`, + explanation: `Merged PR history changes estimated score ${current.scoreEstimate.estimatedMergedScore} -> ${best.scoreEstimate.estimatedMergedScore}.`, + }, + ] + : []), + ...(current.scoreEstimate.issueDiscoveryHistoryMultiplier !== best.scoreEstimate.issueDiscoveryHistoryMultiplier || + current.gates.validSolvedIssues !== best.gates.validSolvedIssues || + current.gates.issueCredibility !== best.gates.issueCredibility + ? [ + { + gate: "issue_discovery_validity_floor" as const, + current: `${current.gates.validSolvedIssues ?? "unknown"} valid solved / ${roundScore(current.gates.issueCredibility ?? 0)} credibility, multiplier ${current.scoreEstimate.issueDiscoveryHistoryMultiplier}`, + projected: `${best.gates.validSolvedIssues ?? "unknown"} valid solved / ${roundScore(best.gates.issueCredibility ?? 0)} credibility, multiplier ${best.scoreEstimate.issueDiscoveryHistoryMultiplier}`, + explanation: `Issue-discovery validity changes estimated score ${current.scoreEstimate.estimatedMergedScore} -> ${best.scoreEstimate.estimatedMergedScore}.`, + }, + ] + : []), ...(current.gates.credibilityObserved !== best.gates.credibilityObserved || current.scoreEstimate.credibilityMultiplier !== best.scoreEstimate.credibilityMultiplier ? [ { @@ -968,6 +1100,15 @@ function normalizeBranchEligibility(input: ScorePreviewInput): BranchEligibility }; } +function resolveMergedPullRequests( + input: Pick, + contributorEvidence?: ContributorEvidenceRecord | null, +): number | undefined { + if (input.mergedPullRequests !== undefined) return nonNegative(input.mergedPullRequests); + const fromEvidence = Number(contributorEvidence?.payload?.mergedPullRequests); + return Number.isFinite(fromEvidence) ? nonNegative(fromEvidence) : undefined; +} + function inferCredibility(evidence?: ContributorEvidenceRecord | null): number { const payload = evidence?.payload; const merged = Number(payload?.mergedPullRequests ?? 0); diff --git a/src/signals/local-branch.ts b/src/signals/local-branch.ts index 98c5cde15a..c947ec9665 100644 --- a/src/signals/local-branch.ts +++ b/src/signals/local-branch.ts @@ -474,6 +474,9 @@ function buildLocalScoreInput(args: { nonCodeLines: nonCodeLineCount, openPrCount: args.outcomeHistory.totals.openPullRequests, openIssueCount: args.repoOutcome?.openIssues ?? args.outcomeHistory.totals.openIssues, + mergedPullRequests: args.repoOutcome?.mergedPullRequests ?? args.outcomeHistory.totals.mergedPullRequests, + validSolvedIssues: args.repoOutcome?.validSolvedIssues ?? args.outcomeHistory.totals.validSolvedIssues, + issueCredibility: args.repoOutcome?.issueCredibility ?? args.outcomeHistory.totals.issueCredibility, credibility: args.repoOutcome?.credibility ?? args.outcomeHistory.totals.credibility, metadataOnly: scorer?.mode !== "gittensor_root" && scorer?.mode !== "external_command", pendingMergedPrCount: args.input.pendingMergedPrCount, @@ -1003,7 +1006,7 @@ function branchQualityBlockersFor(preflight: LocalDiffPreflightResult, localFind function accountStateBlockersFor(scorePreview: ScorePreviewResult): string[] { return scorePreview.blockedBy - .filter((blocker) => ["repo_not_registered", "inactive_allocation", "open_pr_threshold", "credibility_floor"].includes(blocker.code)) + .filter((blocker) => ["repo_not_registered", "inactive_allocation", "open_pr_threshold", "open_issue_threshold", "merged_pr_history_floor", "issue_discovery_validity_floor", "credibility_floor"].includes(blocker.code)) .map((blocker) => blocker.detail) .filter(unique); } diff --git a/test/unit/scoring.test.ts b/test/unit/scoring.test.ts index d25ecc7bf1..ad0ee78606 100644 --- a/test/unit/scoring.test.ts +++ b/test/unit/scoring.test.ts @@ -1398,6 +1398,201 @@ NOVELTY_BONUS_SCALAR = 3 expect(issueDelta?.projected).toContain("multiplier 1"); }); + it("merged-PR history floor blocks scoring when mergedPullRequests is below MIN_VALID_MERGED_PRS (#808)", () => { + const baseInput = { + repoFullName: repo.fullName, + sourceTokenScore: 60, + totalTokenScore: 90, + sourceLines: 50, + openPrCount: 0, + credibility: 1, + }; + + const atFloor = buildScorePreview({ repo, snapshot, input: { ...baseInput, mergedPullRequests: 3 } }); + expect(atFloor.gates.mergedPrFloor).toBe(3); + expect(atFloor.gates.mergedPullRequests).toBe(3); + expect(atFloor.scoreEstimate.mergedHistoryMultiplier).toBe(1); + expect(atFloor.effectiveEstimatedScore).toBeGreaterThan(0); + + const belowFloor = buildScorePreview({ repo, snapshot, input: { ...baseInput, mergedPullRequests: 2 } }); + expect(belowFloor.scoreEstimate.mergedHistoryMultiplier).toBe(0); + expect(belowFloor.effectiveEstimatedScore).toBe(0); + expect(belowFloor.blockedBy.some((b) => b.code === "merged_pr_history_floor")).toBe(true); + expect(belowFloor.recommendation.actions.some((action) => /merged PR history/i.test(action))).toBe(true); + }); + + it("merged-PR history floor does not block when mergedPullRequests is unknown (not supplied and no evidence)", () => { + const preview = buildScorePreview({ + repo, + snapshot, + input: { repoFullName: repo.fullName, sourceTokenScore: 60, totalTokenScore: 90, sourceLines: 50, openPrCount: 0, credibility: 1 }, + }); + expect(preview.gates.mergedPullRequests).toBeUndefined(); + expect(preview.scoreEstimate.mergedHistoryMultiplier).toBe(1); + expect(preview.blockedBy.some((b) => b.code === "merged_pr_history_floor")).toBe(false); + }); + + it("infers mergedPullRequests from contributor evidence when input omits it (#808)", () => { + const eligible = buildScorePreview({ + repo, + snapshot, + input: { repoFullName: repo.fullName, sourceTokenScore: 60, totalTokenScore: 90, sourceLines: 50, openPrCount: 0, credibility: 1 }, + contributorEvidence: { + login: "dev", + generatedAt: "2026-05-23T00:00:00.000Z", + payload: { mergedPullRequests: 4, stalePullRequests: 0, unlinkedPullRequests: 0 }, + }, + }); + expect(eligible.gates.mergedPullRequests).toBe(4); + expect(eligible.scoreEstimate.mergedHistoryMultiplier).toBe(1); + + const ineligible = buildScorePreview({ + repo, + snapshot, + input: { repoFullName: repo.fullName, sourceTokenScore: 60, totalTokenScore: 90, sourceLines: 50, openPrCount: 0, credibility: 1 }, + contributorEvidence: { + login: "newbie", + generatedAt: "2026-05-23T00:00:00.000Z", + payload: { mergedPullRequests: 1, stalePullRequests: 0, unlinkedPullRequests: 0 }, + }, + }); + expect(ineligible.scoreEstimate.mergedHistoryMultiplier).toBe(0); + expect(ineligible.blockedBy.some((b) => b.code === "merged_pr_history_floor")).toBe(true); + }); + + it("issue-discovery validity floor blocks when valid solved issues or issue credibility are below upstream floors (#808)", () => { + const issueDiscoveryRepo: RepositoryRecord = { + ...repo, + registryConfig: { ...repo.registryConfig!, issueDiscoveryShare: 0.25 }, + }; + const baseInput = { + repoFullName: issueDiscoveryRepo.fullName, + sourceTokenScore: 60, + totalTokenScore: 90, + sourceLines: 50, + openPrCount: 0, + credibility: 1, + mergedPullRequests: 5, + linkedIssueMode: "standard" as const, + }; + + const eligible = buildScorePreview({ + repo: issueDiscoveryRepo, + snapshot, + input: { ...baseInput, validSolvedIssues: 3, issueCredibility: 0.85 }, + }); + expect(eligible.scoreEstimate.issueDiscoveryHistoryMultiplier).toBe(1); + expect(eligible.effectiveEstimatedScore).toBeGreaterThan(0); + + const lowValidSolved = buildScorePreview({ + repo: issueDiscoveryRepo, + snapshot, + input: { ...baseInput, validSolvedIssues: 2, issueCredibility: 0.9 }, + }); + expect(lowValidSolved.scoreEstimate.issueDiscoveryHistoryMultiplier).toBe(0); + expect(lowValidSolved.blockedBy.some((b) => b.code === "issue_discovery_validity_floor")).toBe(true); + + const lowIssueCredibility = buildScorePreview({ + repo: issueDiscoveryRepo, + snapshot, + input: { ...baseInput, validSolvedIssues: 4, issueCredibility: 0.7 }, + }); + expect(lowIssueCredibility.scoreEstimate.issueDiscoveryHistoryMultiplier).toBe(0); + }); + + it("issue-discovery validity floor is skipped when issue-discovery is not relevant or history is unknown", () => { + const issueDiscoveryRepo: RepositoryRecord = { + ...repo, + registryConfig: { ...repo.registryConfig!, issueDiscoveryShare: 0.25 }, + }; + const noHistory = buildScorePreview({ + repo: issueDiscoveryRepo, + snapshot, + input: { + repoFullName: issueDiscoveryRepo.fullName, + sourceTokenScore: 60, + totalTokenScore: 90, + sourceLines: 50, + openPrCount: 0, + credibility: 1, + mergedPullRequests: 5, + linkedIssueMode: "standard", + }, + }); + expect(noHistory.scoreEstimate.issueDiscoveryHistoryMultiplier).toBe(1); + + const directPrOnly = buildScorePreview({ + repo, + snapshot, + input: { + repoFullName: repo.fullName, + sourceTokenScore: 60, + totalTokenScore: 90, + sourceLines: 50, + openPrCount: 0, + credibility: 1, + mergedPullRequests: 5, + validSolvedIssues: 0, + issueCredibility: 0.1, + linkedIssueMode: "none", + }, + }); + expect(directPrOnly.scoreEstimate.issueDiscoveryHistoryMultiplier).toBe(1); + }); + + it("bestReasonableCase clears contributor-history gates and surfaces gate deltas (#808)", () => { + const issueDiscoveryRepo: RepositoryRecord = { + ...repo, + registryConfig: { ...repo.registryConfig!, issueDiscoveryShare: 0.2 }, + }; + const preview = buildScorePreview({ + repo: issueDiscoveryRepo, + snapshot, + input: { + repoFullName: issueDiscoveryRepo.fullName, + sourceTokenScore: 60, + totalTokenScore: 90, + sourceLines: 50, + openPrCount: 0, + credibility: 1, + mergedPullRequests: 1, + validSolvedIssues: 1, + issueCredibility: 0.5, + linkedIssueMode: "standard", + }, + }); + const bestReasonable = preview.scenarioPreviews.find((scenario) => scenario.name === "bestReasonableCase"); + expect(bestReasonable?.scoreEstimate.mergedHistoryMultiplier).toBe(1); + expect(bestReasonable?.scoreEstimate.issueDiscoveryHistoryMultiplier).toBe(1); + expect(bestReasonable?.effectiveEstimatedScore).toBeGreaterThan(0); + expect(preview.gateDeltas).toEqual( + expect.arrayContaining([ + expect.objectContaining({ gate: "merged_pr_history_floor" }), + expect.objectContaining({ gate: "issue_discovery_validity_floor" }), + ]), + ); + }); + + it("afterPendingMerges projects mergedPullRequests upward when pending merges are supplied (#808)", () => { + const preview = buildScorePreview({ + repo, + snapshot, + input: { + repoFullName: repo.fullName, + sourceTokenScore: 60, + totalTokenScore: 90, + sourceLines: 50, + openPrCount: 0, + credibility: 1, + mergedPullRequests: 2, + pendingMergedPrCount: 1, + }, + }); + const afterPending = preview.scenarioPreviews.find((scenario) => scenario.name === "afterPendingMerges"); + expect(afterPending?.gates.mergedPullRequests).toBe(3); + expect(afterPending?.scoreEstimate.mergedHistoryMultiplier).toBe(1); + }); + it("all nine issue-discovery constants are modeled and do not surface as upstream drift warnings (#808)", () => { const upstreamSource = [ "TEST_FILE_CONTRIBUTION_WEIGHT = 0.05", From 243689bc8d8947ad84b275f54bdd39b0e78a5e35 Mon Sep 17 00:00:00 2001 From: bohdansolovie Date: Thu, 25 Jun 2026 01:28:58 +0200 Subject: [PATCH 2/3] fix(scoring): scope issue-discovery gate to linked-issue previews (#808) Only apply issue-discovery validity when linkedIssueMode is active, and bump local-branch fixtures to established contributor history so existing scenario tests stay valid under the merged-PR floor. Co-authored-by: Cursor --- src/scoring/preview.ts | 4 +++- test/unit/local-branch.test.ts | 24 ++++++++++++------------ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/scoring/preview.ts b/src/scoring/preview.ts index 96d8a36799..ffe523736a 100644 --- a/src/scoring/preview.ts +++ b/src/scoring/preview.ts @@ -404,7 +404,9 @@ function computeScoreCore( const issueCredibilityFloor = constant(constants, "MIN_ISSUE_CREDIBILITY"); const validSolvedIssuesObserved = input.validSolvedIssues !== undefined ? nonNegative(input.validSolvedIssues) : undefined; const issueCredibilityObserved = input.issueCredibility !== undefined ? clamp(input.issueCredibility, 0, 1) : undefined; - const issueDiscoveryRelevant = issueDiscoveryShare > 0 || (input.linkedIssueMode ?? "none") !== "none"; + // Issue-discovery validity mirrors upstream's separate issue lane — only gate previews that + // actually claim linked-issue / issue-discovery scoring, not every repo with a non-zero share. + const issueDiscoveryRelevant = (input.linkedIssueMode ?? "none") !== "none"; const issueDiscoveryHistoryKnown = validSolvedIssuesObserved !== undefined && issueCredibilityObserved !== undefined; const issueDiscoveryHistoryMultiplier = !issueDiscoveryRelevant || !issueDiscoveryHistoryKnown diff --git a/test/unit/local-branch.test.ts b/test/unit/local-branch.test.ts index afdd92b0fb..71a83d3a2d 100644 --- a/test/unit/local-branch.test.ts +++ b/test/unit/local-branch.test.ts @@ -1868,8 +1868,8 @@ const profile: ContributorProfile = { github: { login: "oktofeesh1", topLanguages: ["TypeScript"], source: "github" }, source: "gittensor_api", registeredRepoActivity: { - pullRequests: 2, - mergedPullRequests: 1, + pullRequests: 6, + mergedPullRequests: 5, issues: 0, reposTouched: [repo.fullName], dominantLabels: ["bug"], @@ -1887,16 +1887,16 @@ const outcomeHistory: ContributorOutcomeHistory = { generatedAt: "2026-05-25T00:00:00.000Z", source: "gittensor_api", totals: { - pullRequests: 2, - mergedPullRequests: 1, + pullRequests: 6, + mergedPullRequests: 5, openPullRequests: 0, closedPullRequests: 1, closedPullRequestRate: 0.5, issues: 0, openIssues: 0, closedIssues: 0, - solvedIssues: 0, - validSolvedIssues: 0, + solvedIssues: 3, + validSolvedIssues: 3, credibility: 0.92, issueCredibility: 1, }, @@ -1906,16 +1906,16 @@ const outcomeHistory: ContributorOutcomeHistory = { role: "outside_contributor", lane: "direct_pr", maintainerLane: false, - pullRequests: 2, - mergedPullRequests: 1, + pullRequests: 6, + mergedPullRequests: 5, openPullRequests: 0, closedPullRequests: 1, closedPullRequestRate: 0.5, issues: 0, openIssues: 0, closedIssues: 0, - solvedIssues: 0, - validSolvedIssues: 0, + solvedIssues: 3, + validSolvedIssues: 3, credibility: 0.92, issueCredibility: 1, isEligible: true, @@ -1962,8 +1962,8 @@ const scoringProfile: ContributorScoringProfile = { generatedAt: "2026-05-25T00:00:00.000Z", scoringModelSnapshotId: "scoring-test", evidence: { - registeredRepoPullRequests: 2, - mergedPullRequests: 1, + registeredRepoPullRequests: 6, + mergedPullRequests: 5, openPullRequests: 0, stalePullRequests: 0, unlinkedPullRequests: 0, From cd8719cc20084ed65afd6f4e05d3fe1c29d2c66b Mon Sep 17 00:00:00 2001 From: bohdansolovie Date: Thu, 25 Jun 2026 01:41:45 +0200 Subject: [PATCH 3/3] test(scoring): reach codecov patch threshold for validity gates (#808) Remove dead nullish-coalescing branches in gate blockers/deltas and add coverage for contributor-history wiring, public blocker text, and actions. Co-authored-by: Cursor --- src/scoring/preview.ts | 19 ++++----- test/unit/local-branch.test.ts | 66 ++++++++++++++++++++++++++++++ test/unit/scenario-summary.test.ts | 29 +++++++++++++ test/unit/scoring.test.ts | 4 ++ 4 files changed, 107 insertions(+), 11 deletions(-) diff --git a/src/scoring/preview.ts b/src/scoring/preview.ts index ffe523736a..d874e616b8 100644 --- a/src/scoring/preview.ts +++ b/src/scoring/preview.ts @@ -725,7 +725,7 @@ function blockedByFor(input: ScorePreviewInput, repo: RepositoryRecord | null, c { code: "merged_pr_history_floor" as const, severity: "blocker" as const, - detail: `Merged PR count ${core.gates.mergedPullRequests ?? 0} is below upstream floor ${core.gates.mergedPrFloor}.`, + detail: `Merged PR count ${core.gates.mergedPullRequests} is below upstream floor ${core.gates.mergedPrFloor}.`, }, ] : []), @@ -734,7 +734,7 @@ function blockedByFor(input: ScorePreviewInput, repo: RepositoryRecord | null, c { code: "issue_discovery_validity_floor" as const, severity: "blocker" as const, - detail: `Issue-discovery history (${core.gates.validSolvedIssues ?? 0} valid solved, credibility ${roundScore(core.gates.issueCredibility ?? 0)}) is below upstream floors (${core.gates.validSolvedIssuesFloor} valid solved, ${core.gates.issueCredibilityFloor} credibility).`, + detail: `Issue-discovery history (${core.gates.validSolvedIssues} valid solved, credibility ${roundScore(core.gates.issueCredibility!)}) is below upstream floors (${core.gates.validSolvedIssuesFloor} valid solved, ${core.gates.issueCredibilityFloor} credibility).`, }, ] : []), @@ -820,25 +820,22 @@ function buildGateDeltas(current: ScoreCore, scenarios: ScoreScenarioPreview[]): }, ] : []), - ...(current.scoreEstimate.mergedHistoryMultiplier !== best.scoreEstimate.mergedHistoryMultiplier || - current.gates.mergedPullRequests !== best.gates.mergedPullRequests + ...(current.scoreEstimate.mergedHistoryMultiplier !== best.scoreEstimate.mergedHistoryMultiplier ? [ { gate: "merged_pr_history_floor" as const, - current: `${current.gates.mergedPullRequests ?? "unknown"}/${current.gates.mergedPrFloor} merged PRs, multiplier ${current.scoreEstimate.mergedHistoryMultiplier}`, - projected: `${best.gates.mergedPullRequests ?? "unknown"}/${best.gates.mergedPrFloor} merged PRs, multiplier ${best.scoreEstimate.mergedHistoryMultiplier}`, + current: `${current.gates.mergedPullRequests}/${current.gates.mergedPrFloor} merged PRs, multiplier ${current.scoreEstimate.mergedHistoryMultiplier}`, + projected: `${best.gates.mergedPullRequests}/${best.gates.mergedPrFloor} merged PRs, multiplier ${best.scoreEstimate.mergedHistoryMultiplier}`, explanation: `Merged PR history changes estimated score ${current.scoreEstimate.estimatedMergedScore} -> ${best.scoreEstimate.estimatedMergedScore}.`, }, ] : []), - ...(current.scoreEstimate.issueDiscoveryHistoryMultiplier !== best.scoreEstimate.issueDiscoveryHistoryMultiplier || - current.gates.validSolvedIssues !== best.gates.validSolvedIssues || - current.gates.issueCredibility !== best.gates.issueCredibility + ...(current.scoreEstimate.issueDiscoveryHistoryMultiplier !== best.scoreEstimate.issueDiscoveryHistoryMultiplier ? [ { gate: "issue_discovery_validity_floor" as const, - current: `${current.gates.validSolvedIssues ?? "unknown"} valid solved / ${roundScore(current.gates.issueCredibility ?? 0)} credibility, multiplier ${current.scoreEstimate.issueDiscoveryHistoryMultiplier}`, - projected: `${best.gates.validSolvedIssues ?? "unknown"} valid solved / ${roundScore(best.gates.issueCredibility ?? 0)} credibility, multiplier ${best.scoreEstimate.issueDiscoveryHistoryMultiplier}`, + current: `${current.gates.validSolvedIssues} valid solved / ${roundScore(current.gates.issueCredibility!)} credibility, multiplier ${current.scoreEstimate.issueDiscoveryHistoryMultiplier}`, + projected: `${best.gates.validSolvedIssues} valid solved / ${roundScore(best.gates.issueCredibility!)} credibility, multiplier ${best.scoreEstimate.issueDiscoveryHistoryMultiplier}`, explanation: `Issue-discovery validity changes estimated score ${current.scoreEstimate.estimatedMergedScore} -> ${best.scoreEstimate.estimatedMergedScore}.`, }, ] diff --git a/test/unit/local-branch.test.ts b/test/unit/local-branch.test.ts index 71a83d3a2d..f66f8fad48 100644 --- a/test/unit/local-branch.test.ts +++ b/test/unit/local-branch.test.ts @@ -122,6 +122,72 @@ describe("local branch analysis", () => { expect(analysis.scorePreview.blockedBy).toEqual(expect.arrayContaining([expect.objectContaining({ code: "open_issue_threshold" })])); }); + it("threads contributor-history validity gates from outcome history (#808)", () => { + const analysis = buildLocalBranchAnalysis({ + input: { + login: "oktofeesh1", + repoFullName: repo.fullName, + body: "Fixes #7", + changedFiles: [{ path: "src/cache.ts", additions: 42, deletions: 4, status: "modified" }], + localScorer: { mode: "external_command", sourceTokenScore: 48, totalTokenScore: 80, sourceLines: 46 }, + }, + repo, + issues: [{ repoFullName: repo.fullName, number: 7, title: "Cache refresh fails", state: "open", labels: ["bug"], linkedPrs: [] }], + pullRequests: [], + profile, + outcomeHistory, + scoringSnapshot, + scoringProfile, + }); + expect(analysis.scorePreview.gates.mergedPullRequests).toBe(5); + expect(analysis.scorePreview.gates.validSolvedIssues).toBe(3); + expect(analysis.scorePreview.gates.issueCredibility).toBe(1); + expect(analysis.scorePreview.scoreEstimate.mergedHistoryMultiplier).toBe(1); + expect(analysis.scorePreview.scoreEstimate.issueDiscoveryHistoryMultiplier).toBe(1); + }); + + it("prefers repo-scoped outcome counts over global totals when both are present (#808)", () => { + const repoScopedHistory: ContributorOutcomeHistory = { + ...outcomeHistory, + totals: { + ...outcomeHistory.totals, + mergedPullRequests: 5, + validSolvedIssues: 3, + issueCredibility: 1, + }, + repoOutcomes: [ + { + ...outcomeHistory.repoOutcomes[0]!, + mergedPullRequests: 1, + validSolvedIssues: 0, + issueCredibility: 0.4, + }, + ], + }; + const analysis = buildLocalBranchAnalysis({ + input: { + login: "oktofeesh1", + repoFullName: repo.fullName, + body: "Fixes #7", + changedFiles: [{ path: "src/cache.ts", additions: 42, deletions: 4, status: "modified" }], + localScorer: { mode: "external_command", sourceTokenScore: 48, totalTokenScore: 80, sourceLines: 46 }, + }, + repo, + issues: [{ repoFullName: repo.fullName, number: 7, title: "Cache refresh fails", state: "open", labels: ["bug"], linkedPrs: [] }], + pullRequests: [], + profile, + outcomeHistory: repoScopedHistory, + scoringSnapshot, + scoringProfile, + }); + expect(analysis.scorePreview.gates.mergedPullRequests).toBe(1); + expect(analysis.scorePreview.gates.validSolvedIssues).toBe(0); + expect(analysis.scorePreview.gates.issueCredibility).toBe(0.4); + expect(analysis.scorePreview.scoreEstimate.mergedHistoryMultiplier).toBe(0); + expect(analysis.scorePreview.scoreEstimate.issueDiscoveryHistoryMultiplier).toBe(0); + expect(analysis.accountStateBlockers.join(" ")).toMatch(/Merged PR count|Issue-discovery history/i); + }); + it("bounds local scorer warnings before adding local findings", () => { const analysis = buildLocalBranchAnalysis({ input: { diff --git a/test/unit/scenario-summary.test.ts b/test/unit/scenario-summary.test.ts index c351bbb99e..db6696a787 100644 --- a/test/unit/scenario-summary.test.ts +++ b/test/unit/scenario-summary.test.ts @@ -359,6 +359,35 @@ describe("renderPublicScenarioSummary", () => { expect(summary.eligibilityNotes.join(" ")).not.toMatch(/Validating the linked issue would enable/i); }); + it("maps contributor-history validity blockers to public-safe notes (#808)", () => { + const preview = buildScorePreview({ + repo, + snapshot, + input: { + repoFullName: "octo/demo", + sourceTokenScore: 60, + totalTokenScore: 80, + sourceLines: 50, + openPrCount: 0, + credibility: 1, + mergedPullRequests: 1, + validSolvedIssues: 1, + issueCredibility: 0.5, + linkedIssueMode: "standard", + linkedIssueContext: { status: "raw", source: "user_supplied", issueNumbers: [77] }, + }, + }); + const summary = renderPublicScenarioSummary({ + repoFullName: "octo/demo", + generatedAt: "2026-06-03T00:00:00.000Z", + publicBlockers: preview.blockedBy.filter((blocker) => + ["merged_pr_history_floor", "issue_discovery_validity_floor"].includes(blocker.code), + ), + }); + expect(summary.blockerNotes.join(" ")).toMatch(/Merged PR history on this repo is below the upstream eligibility floor/i); + expect(summary.blockerNotes.join(" ")).toMatch(/Valid solved-issue history or issue .* is below the upstream issue-discovery floor/i); + }); + it("omits projected open-count notes when pending detection has no after-cleanup projection", () => { const summary = renderPublicScenarioSummary({ repoFullName: "octo/demo", diff --git a/test/unit/scoring.test.ts b/test/unit/scoring.test.ts index ad0ee78606..e4e53ffcfd 100644 --- a/test/unit/scoring.test.ts +++ b/test/unit/scoring.test.ts @@ -1498,6 +1498,10 @@ NOVELTY_BONUS_SCALAR = 3 input: { ...baseInput, validSolvedIssues: 4, issueCredibility: 0.7 }, }); expect(lowIssueCredibility.scoreEstimate.issueDiscoveryHistoryMultiplier).toBe(0); + expect(lowIssueCredibility.blockedBy.some((b) => b.code === "issue_discovery_validity_floor")).toBe(true); + expect( + lowIssueCredibility.recommendation.actions.some((action) => /valid solved-issue history and issue credibility/i.test(action)), + ).toBe(true); }); it("issue-discovery validity floor is skipped when issue-discovery is not relevant or history is unknown", () => {