diff --git a/src/services/decision-pack.ts b/src/services/decision-pack.ts index 0001ae19a4..3166293c71 100644 --- a/src/services/decision-pack.ts +++ b/src/services/decision-pack.ts @@ -23,7 +23,7 @@ import { } from "../db/repositories"; import { contributorRepoStatsFromGittensor, fetchGittensorContributorSnapshot } from "../gittensor/api"; import { fetchPublicContributorProfile } from "../github/public"; -import { getOrCreateScoringModelSnapshot } from "../scoring/model"; +import { DEFAULT_SCORING_CONSTANTS, getOrCreateScoringModelSnapshot } from "../scoring/model"; import { buildContributorFit, buildContributorOutcomeHistory, @@ -70,6 +70,12 @@ import { nowIso } from "../utils/json"; export const CONTRIBUTOR_DECISION_PACK_SIGNAL = "contributor-decision-pack"; export const DECISION_PACK_MAX_AGE_MS = 6 * 60 * 60 * 1000; +const DEFAULT_OSS_EMISSION_SHARE = DEFAULT_SCORING_CONSTANTS.OSS_EMISSION_SHARE ?? 0.9; + +function resolveOssEmissionShare(constants: Record | undefined): number { + const value = constants?.OSS_EMISSION_SHARE; + return typeof value === "number" && Number.isFinite(value) ? value : DEFAULT_OSS_EMISSION_SHARE; +} export const DECISION_PACK_REBUILD_DEBOUNCE_MS = 15 * 1000; const pendingDecisionPackRebuilds = new Map>(); @@ -469,6 +475,7 @@ export async function buildAndPersistContributorDecisionPack(env: Env, login: st totals, opportunities: fit.opportunities, scoringModelSnapshotId: scoringSnapshot.id, + ossEmissionShare: resolveOssEmissionShare(scoringSnapshot.constants), contributorPullRequests, contributorIssues, repoStats, @@ -546,6 +553,7 @@ function buildContributorDecisionPack(args: { focusManifests?: Map | undefined; repoOutcomePatternsByRepo?: Map | undefined; recommendationOutcomeFeedback?: AgentRecommendationOutcomeSummary | undefined; + ossEmissionShare?: number | undefined; }): ContributorDecisionPack { const recommendationOutcomeFeedback = args.recommendationOutcomeFeedback ?? emptyRecommendationOutcomeFeedback(args.login); const registeredRepositories = args.repositories.filter((repo) => repo.isRegistered); @@ -584,6 +592,7 @@ function buildContributorDecisionPack(args: { focusManifest: args.focusManifests?.get(key), repoOutcomePatterns: args.repoOutcomePatternsByRepo?.get(key), recommendationOutcomeFeedback: recommendationFeedbackByRepo.get(key), + ossEmissionShare: args.ossEmissionShare, }); }) .sort((left, right) => right.priorityScore - left.priorityScore || left.repoFullName.localeCompare(right.repoFullName)); @@ -665,9 +674,12 @@ function buildRepoDecision(args: { focusManifest?: FocusManifest | undefined; repoOutcomePatterns?: RepoOutcomePatterns | undefined; recommendationOutcomeFeedback?: AgentRecommendationOutcomeRepoSummary | undefined; + ossEmissionShare?: number | undefined; }): RepoDecision { const lane = buildLaneAdvice(args.repo, args.repo.fullName); const config = args.repo.registryConfig; + const ossEmissionShare = + typeof args.ossEmissionShare === "number" && Number.isFinite(args.ossEmissionShare) ? args.ossEmissionShare : DEFAULT_OSS_EMISSION_SHARE; const openPullRequests = args.totals?.openPullRequestsTotal ?? args.syncState?.openPullRequestsCount ?? 0; const openIssues = args.totals?.openIssuesTotal ?? args.syncState?.openIssuesCount ?? 0; const queue = { @@ -676,10 +688,14 @@ function buildRepoDecision(args: { mergedPullRequests: args.totals?.mergedPullRequestsTotal ?? args.syncState?.recentMergedPullRequestsCount ?? 0, closedUnmergedPullRequests: args.totals?.closedUnmergedPullRequestsTotal ?? 0, }; + const baseEmissionShare = config?.emissionShare ?? 0; + // Lane shares are a split of the OSS *mining* pool (emissionShare * OSS_EMISSION_SHARE), matching + // preview.ts laneMath (directPrSlice/issueDiscoverySlice) and reward-risk.ts. The raw emissionShare + // field stays raw (it mirrors laneMath.repoEmissionShare). const rewardUpside = { - emissionShare: round(config?.emissionShare ?? 0), - directPrShare: round((config?.emissionShare ?? 0) * (1 - (config?.issueDiscoveryShare ?? 0))), - issueDiscoveryShare: round((config?.emissionShare ?? 0) * (config?.issueDiscoveryShare ?? 0)), + emissionShare: round(baseEmissionShare), + directPrShare: round(baseEmissionShare * ossEmissionShare * (1 - (config?.issueDiscoveryShare ?? 0))), + issueDiscoveryShare: round(baseEmissionShare * ossEmissionShare * (config?.issueDiscoveryShare ?? 0)), maintainerCut: round(config?.maintainerCut ?? 0), }; const blockers = scoreBlockersFor(args.repo.fullName, lane.lane, args.roleContext, args.outcome); diff --git a/test/unit/decision-pack.test.ts b/test/unit/decision-pack.test.ts index 9ac4e5d07e..7e5f01cd7e 100644 --- a/test/unit/decision-pack.test.ts +++ b/test/unit/decision-pack.test.ts @@ -96,7 +96,7 @@ describe("decision-pack service", () => { outcome: undefined, syncState: { openPullRequestsCount: 1, openIssuesCount: 2, recentMergedPullRequestsCount: 3 } as any, }), - ).toMatchObject({ recommendation: "watch", queue: { openPullRequests: 1, openIssues: 2, mergedPullRequests: 3 }, rewardUpside: { issueDiscoveryShare: 0.02 } }); + ).toMatchObject({ recommendation: "watch", queue: { openPullRequests: 1, openIssues: 2, mergedPullRequests: 3 }, rewardUpside: { issueDiscoveryShare: 0.018 } }); expect( __decisionPackInternals.buildRepoDecision({ repo: repo("owner/inactive", 0, 0), @@ -111,6 +111,31 @@ describe("decision-pack service", () => { expect(__decisionPackInternals.round(1.23456)).toBe(1.2346); }); + it("applies OSS_EMISSION_SHARE to rewardUpside lane shares (not the raw emission share)", () => { + const outsideRole = { maintainerLane: false } as any; + // Split repo: emissionShare 0.04, issueDiscoveryShare 0.25. Lane shares are a split of the OSS + // mining pool (emissionShare * 0.9), matching preview.ts laneMath / reward-risk.ts -- NOT the raw split. + const split = __decisionPackInternals.buildRepoDecision({ + repo: repo("owner/split", 0.04, 0.25), + roleContext: outsideRole, + outcome: undefined, + }).rewardUpside; + expect(split.emissionShare).toBe(0.04); // raw value preserved (mirrors laneMath.repoEmissionShare) + expect(split.directPrShare).toBeCloseTo(0.04 * 0.9 * 0.75, 10); // 0.027 + expect(split.issueDiscoveryShare).toBeCloseTo(0.04 * 0.9 * 0.25, 10); // 0.009 + // A single lane share can never exceed the repo's whole OSS mining pool (emissionShare * 0.9). + expect(split.directPrShare).toBeLessThanOrEqual(0.04 * 0.9 + 1e-9); + + // An explicit snapshot-provided OSS_EMISSION_SHARE override is honored. + const overridden = __decisionPackInternals.buildRepoDecision({ + repo: repo("owner/override", 0.04, 0), + roleContext: outsideRole, + outcome: undefined, + ossEmissionShare: 0.8, + }).rewardUpside; + expect(overridden.directPrShare).toBeCloseTo(0.04 * 0.8, 10); // 0.032 + }); + it("feeds repo outcome patterns into repo decisions without inflating maintainer-lane evidence", () => { const outsideRole = { maintainerLane: false } as any; const maintainerRole = { maintainerLane: true } as any; @@ -1048,7 +1073,8 @@ describe("decision-pack service", () => { summary: "0 open issues evaluated.", }, }); - expect(emptyQualityDecision.priorityScore).toBe(40); + // 38, not 40: upside = issueDiscoveryShare (0.02 * OSS_EMISSION_SHARE 0.9 = 0.018) * 1000 = 18. + expect(emptyQualityDecision.priorityScore).toBe(38); }); it("issues avoid_for_now reasoning with sanitized public copy", () => {