diff --git a/src/signals/engine.ts b/src/signals/engine.ts index 4bf27301c2..39c59b4574 100644 --- a/src/signals/engine.ts +++ b/src/signals/engine.ts @@ -1540,8 +1540,11 @@ export function buildContributorOutcomeHistory(args: { const mergedPullRequests = official?.mergedPullRequests ?? Math.max(cachedPrs.filter((pr) => pr.mergedAt || pr.state === "merged").length, cachedStat?.mergedPullRequests ?? 0); const openPullRequests = official?.openPullRequests ?? Math.max(cachedPrs.filter((pr) => pr.state === "open").length, cachedStat?.openPullRequests ?? 0); const closedPullRequests = official?.closedPullRequests ?? Math.max(cachedPrs.filter((pr) => pr.state === "closed" && !pr.mergedAt).length, pullRequests - mergedPullRequests - openPullRequests, 0); - const openIssues = official?.openIssues ?? cachedIssues.filter((issue) => issue.state === "open").length; - const closedIssues = official?.closedIssues ?? cachedIssues.filter((issue) => issue.state !== "open").length; + const openIssueRows = cachedIssues.filter((issue) => issue.state === "open").length; + const closedIssueRows = cachedIssues.filter((issue) => issue.state !== "open").length; + const cachedIssueCount = Math.max(cachedIssues.length, cachedStat?.issues ?? 0); + const openIssues = official?.openIssues ?? openIssueRows; + const closedIssues = official?.closedIssues ?? Math.max(closedIssueRows, cachedIssueCount - openIssueRows, 0); // Like every field above, issue-discovery solved counts fall back to cache (the issue // lifecycle), not a literal 0, when official Gittensor data is absent for this repo. const laneAdvice = buildLaneAdvice(repo, repoFullName); diff --git a/test/unit/signals-v2.test.ts b/test/unit/signals-v2.test.ts index 0aab5a7273..7cff271c1f 100644 --- a/test/unit/signals-v2.test.ts +++ b/test/unit/signals-v2.test.ts @@ -807,6 +807,29 @@ describe("v2 signal builders", () => { expect(t.openPullRequests).toBe(1); // the stranger's 5 open PRs are excluded }); + it("uses aggregate repo-stat issues for cache-only outcome totals when issue rows are absent", () => { + const docsRepo: RepositoryRecord = { + fullName: "acme/docs", + owner: "acme", + name: "docs", + isInstalled: true, + isRegistered: true, + isPrivate: false, + defaultBranch: "main", + registryConfig: { repo: "acme/docs", emissionShare: 0.02, issueDiscoveryShare: 0, labelMultipliers: {}, trustedLabelPipeline: false, maintainerCut: 0, raw: {} }, + }; + const repoStats: ContributorRepoStatRecord[] = [ + { login: "statdev", repoFullName: "acme/docs", pullRequests: 0, mergedPullRequests: 0, openPullRequests: 0, issues: 7, stalePullRequests: 0, unlinkedPullRequests: 0, dominantLabels: ["docs"] }, + ]; + const profile = buildContributorProfile("statdev", { login: "statdev", topLanguages: ["Markdown"], source: "github" }, [], [], repoStats); + const history = buildContributorOutcomeHistory({ login: "statdev", profile, repositories: [docsRepo], pullRequests: [], issues: [], repoStats }); + + expect(profile.registeredRepoActivity.issues).toBe(7); + expect(history.repoOutcomes.find((entry) => entry.repoFullName === "acme/docs")).toMatchObject({ issues: 7, openIssues: 0, closedIssues: 7 }); + expect(history.totals).toMatchObject({ issues: 7, openIssues: 0, closedIssues: 7 }); + expect(history.reconciliation?.repos.find((entry) => entry.repoFullName === "acme/docs")?.cached).toMatchObject({ issues: 7, openIssues: 0, closedIssues: 7 }); + }); + 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];