From 3f533de397dd9f08e6083a67a7b29d11dd110b64 Mon Sep 17 00:00:00 2001 From: philluiz2323 Date: Thu, 4 Jun 2026 04:22:09 -0700 Subject: [PATCH] Derive cache-only contributor outcome totals consistently from repoOutcomes --- src/signals/engine.ts | 27 ++++++++++++++++-------- test/unit/signals-v2.test.ts | 41 ++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/signals/engine.ts b/src/signals/engine.ts index 014e0702..c0702296 100644 --- a/src/signals/engine.ts +++ b/src/signals/engine.ts @@ -1536,17 +1536,26 @@ export function buildContributorOutcomeHistory(args: { }; }) .filter((outcome) => outcome.pullRequests + outcome.issues > 0 || outcome.maintainerLane); + // When official Gittensor totals are absent, derive every PR/issue total from the same + // login-scoped, internally-consistent repoOutcomes (each repo keeps pullRequests >= + // merged + open + closed and issues = openIssues + closedIssues). Previously pullRequests/ + // mergedPullRequests fell back to registeredRepoActivity and openPullRequests to an + // unfiltered repoStats sum, breaking the invariant and letting closedPullRequestRate exceed 1. + const sumOutcomes = (pick: (outcome: (typeof repoOutcomes)[number]) => number): number => repoOutcomes.reduce((sum, outcome) => sum + pick(outcome), 0); + const gittensorTotals = args.profile.gittensor?.totals; + const openIssues = gittensorTotals?.openIssues ?? sumOutcomes((outcome) => outcome.openIssues); + const closedIssues = gittensorTotals?.closedIssues ?? sumOutcomes((outcome) => outcome.closedIssues); const totals = { - pullRequests: args.profile.gittensor?.totals.pullRequests ?? args.profile.registeredRepoActivity.pullRequests, - mergedPullRequests: args.profile.gittensor?.totals.mergedPullRequests ?? args.profile.registeredRepoActivity.mergedPullRequests, - openPullRequests: args.profile.gittensor?.totals.openPullRequests ?? args.repoStats.reduce((sum, stat) => sum + stat.openPullRequests, 0), - closedPullRequests: args.profile.gittensor?.totals.closedPullRequests ?? repoOutcomes.reduce((sum, outcome) => sum + outcome.closedPullRequests, 0), + pullRequests: gittensorTotals?.pullRequests ?? sumOutcomes((outcome) => outcome.pullRequests), + mergedPullRequests: gittensorTotals?.mergedPullRequests ?? sumOutcomes((outcome) => outcome.mergedPullRequests), + openPullRequests: gittensorTotals?.openPullRequests ?? sumOutcomes((outcome) => outcome.openPullRequests), + closedPullRequests: gittensorTotals?.closedPullRequests ?? sumOutcomes((outcome) => outcome.closedPullRequests), closedPullRequestRate: 0, - issues: args.profile.registeredRepoActivity.issues, - openIssues: args.profile.gittensor?.totals.openIssues ?? repoOutcomes.reduce((sum, outcome) => sum + outcome.openIssues, 0), - closedIssues: args.profile.gittensor?.totals.closedIssues ?? repoOutcomes.reduce((sum, outcome) => sum + outcome.closedIssues, 0), - solvedIssues: args.profile.gittensor?.totals.solvedIssues ?? repoOutcomes.reduce((sum, outcome) => sum + outcome.solvedIssues, 0), - validSolvedIssues: args.profile.gittensor?.totals.validSolvedIssues ?? repoOutcomes.reduce((sum, outcome) => sum + outcome.validSolvedIssues, 0), + issues: openIssues + closedIssues, + openIssues, + closedIssues, + solvedIssues: gittensorTotals?.solvedIssues ?? sumOutcomes((outcome) => outcome.solvedIssues), + validSolvedIssues: gittensorTotals?.validSolvedIssues ?? sumOutcomes((outcome) => outcome.validSolvedIssues), credibility: args.profile.gittensor?.credibility ?? 0, issueCredibility: args.profile.gittensor?.issueCredibility ?? 0, }; diff --git a/test/unit/signals-v2.test.ts b/test/unit/signals-v2.test.ts index 79244d24..5e535a48 100644 --- a/test/unit/signals-v2.test.ts +++ b/test/unit/signals-v2.test.ts @@ -740,6 +740,47 @@ describe("v2 signal builders", () => { expect(history.reconciliation?.repos[0]?.discrepancyReasons).toEqual(expect.arrayContaining([expect.stringContaining("Official source unavailable")])); }); + it("derives cache-only totals consistently and login-scoped (pullRequests = merged + open + closed)", () => { + const widgetRepo: RepositoryRecord = { + fullName: "acme/widgets", + owner: "acme", + name: "widgets", + isInstalled: true, + isRegistered: true, + isPrivate: false, + defaultBranch: "main", + registryConfig: { repo: "acme/widgets", emissionShare: 0.02, issueDiscoveryShare: 0, labelMultipliers: {}, trustedLabelPipeline: false, maintainerCut: 0, raw: {} }, + }; + const mk = (number: number, state: string, extra: Partial = {}): PullRequestRecord => ({ + repoFullName: "acme/widgets", + number, + title: `PR ${number}`, + state, + authorLogin: "dev", + authorAssociation: "NONE", + labels: [], + linkedIssues: [], + body: "", + updatedAt: "2026-05-01T00:00:00.000Z", + ...extra, + }); + const prs = [mk(1, "merged", { mergedAt: "2026-05-01T00:00:00.000Z" }), mk(2, "open"), mk(3, "closed")]; + const profile = buildContributorProfile("dev", { login: "dev", topLanguages: ["TypeScript"], source: "github" }, prs, []); + // repoStats includes a DIFFERENT login that must not leak into this contributor's totals. + const repoStats: ContributorRepoStatRecord[] = [ + { login: "dev", repoFullName: "acme/widgets", pullRequests: 3, mergedPullRequests: 1, openPullRequests: 1, issues: 0, stalePullRequests: 0, unlinkedPullRequests: 0, dominantLabels: [] }, + { login: "stranger", repoFullName: "acme/tools", pullRequests: 5, mergedPullRequests: 0, openPullRequests: 5, issues: 0, stalePullRequests: 0, unlinkedPullRequests: 0, dominantLabels: [] }, + ]; + const history = buildContributorOutcomeHistory({ login: "dev", profile, repositories: [widgetRepo], pullRequests: prs, issues: [], repoStats }); + + const t = history.totals; + // Invariant, login-scoping, and bounded rate were all broken by the mixed-source fallbacks. + expect(t.pullRequests).toBe(t.mergedPullRequests + t.openPullRequests + t.closedPullRequests); + expect(t.issues).toBe(t.openIssues + t.closedIssues); + expect(t.closedPullRequestRate).toBeLessThanOrEqual(1); + expect(t.openPullRequests).toBe(1); // the stranger's 5 open PRs are excluded + }); + it("derives solved and valid-solved issue-discovery counts from cache when official data is absent", () => { const mkRepo = (fullName: string, issueDiscoveryShare: number): RepositoryRecord => { const [owner, name] = fullName.split("/") as [string, string];