From e380346b7b3be483ac16438b8f912959e88223fd Mon Sep 17 00:00:00 2001 From: oktofeesh1 <287075021+oktofeesh1@users.noreply.github.com> Date: Fri, 29 May 2026 22:19:04 -0700 Subject: [PATCH 1/4] feat(data): add contributor reconciliation reports Compare official contributor totals with cached GitHub context and keep maintainer-lane history separated in private outcome data. --- src/signals/engine.ts | 204 +++++++++++++++++++++++++++++++++++ test/unit/signals-v2.test.ts | 24 +++++ 2 files changed, 228 insertions(+) diff --git a/src/signals/engine.ts b/src/signals/engine.ts index f114bd9464..4c59bbfeb7 100644 --- a/src/signals/engine.ts +++ b/src/signals/engine.ts @@ -197,6 +197,7 @@ export type ContributorOutcomeHistory = { login: string; generatedAt: string; source: ContributorProfile["source"]; + reconciliation?: ContributorReconciliationReport | undefined; totals: { pullRequests: number; mergedPullRequests: number; @@ -238,6 +239,37 @@ export type ContributorOutcomeHistory = { summary: string; }; +type ContributorOutcomeCounts = Pick< + ContributorOutcomeHistory["repoOutcomes"][number], + "pullRequests" | "mergedPullRequests" | "openPullRequests" | "closedPullRequests" | "issues" | "openIssues" | "closedIssues" | "solvedIssues" | "validSolvedIssues" +>; + +export type ContributorReconciliationReport = { + login: string; + generatedAt: string; + source: ContributorProfile["source"]; + officialAuthoritative: boolean; + totals: { + official?: ContributorOutcomeHistory["totals"] | undefined; + cached: ContributorOutcomeHistory["totals"]; + effective: ContributorOutcomeHistory["totals"]; + }; + repos: Array<{ + repoFullName: string; + maintainerLane: boolean; + official?: ContributorOutcomeCounts | undefined; + cached: ContributorOutcomeCounts; + effective: ContributorOutcomeCounts; + discrepancyReasons: string[]; + freshness: { + officialUpdatedAt?: string | undefined; + cachedLastActivityAt?: string | undefined; + }; + }>; + findings: SignalFinding[]; + summary: string; +}; + export type OutcomePattern = { repoFullName?: string | undefined; title: string; @@ -1371,6 +1403,7 @@ export function buildContributorOutcomeHistory(args: { login: args.login, generatedAt: nowIso(), source: args.profile.source, + reconciliation: undefined as ContributorReconciliationReport | undefined, totals, repoOutcomes, successPatterns: [] as OutcomePattern[], @@ -1379,10 +1412,177 @@ export function buildContributorOutcomeHistory(args: { }; history.successPatterns = outcomeSuccessPatterns(history); history.failurePatterns = outcomeFailurePatterns(history); + history.reconciliation = buildContributorReconciliationReport({ ...args, history }); history.summary = `${args.login} has ${totals.pullRequests} official/cached PR(s), ${totals.mergedPullRequests} merged, ${totals.closedPullRequests} closed, ${totals.openPullRequests} open, and ${history.repoOutcomes.length} repo-specific outcome profile(s).`; return history; } +export function buildContributorReconciliationReport(args: { + login: string; + profile: ContributorProfile; + repositories: RepositoryRecord[]; + pullRequests: PullRequestRecord[]; + issues: IssueRecord[]; + repoStats: ContributorRepoStatRecord[]; + history?: ContributorOutcomeHistory | undefined; +}): ContributorReconciliationReport { + const repoNames = new Set(); + for (const repo of args.profile.gittensor?.repositories ?? []) repoNames.add(repo.repoFullName); + for (const stat of args.repoStats.filter((stat) => sameLogin(stat.login, args.login))) repoNames.add(stat.repoFullName); + for (const pr of args.pullRequests.filter((pr) => sameLogin(pr.authorLogin, args.login))) repoNames.add(pr.repoFullName); + for (const issue of args.issues.filter((issue) => sameLogin(issue.authorLogin, args.login))) repoNames.add(issue.repoFullName); + const officialByRepo = new Map(args.profile.gittensor?.repositories.map((repo) => [repo.repoFullName.toLowerCase(), repo]) ?? []); + const statByRepo = new Map(args.repoStats.filter((stat) => sameLogin(stat.login, args.login)).map((stat) => [stat.repoFullName.toLowerCase(), stat])); + const repoByName = new Map(args.repositories.map((repo) => [repo.fullName.toLowerCase(), repo])); + const repos = [...repoNames].sort((left, right) => left.localeCompare(right)).map((repoFullName) => { + const key = repoFullName.toLowerCase(); + const official = officialByRepo.get(key); + const cached = cachedReconciliationCounts(args.login, repoFullName, args.pullRequests, args.issues, statByRepo.get(key)); + const officialCounts = official + ? { + pullRequests: official.pullRequests, + mergedPullRequests: official.mergedPullRequests, + openPullRequests: official.openPullRequests, + closedPullRequests: official.closedPullRequests, + issues: official.openIssues + official.closedIssues, + openIssues: official.openIssues, + closedIssues: official.closedIssues, + solvedIssues: official.solvedIssues, + validSolvedIssues: official.validSolvedIssues, + } + : undefined; + const maintainerLane = + sameLogin(repoByName.get(key)?.owner, args.login) || + args.pullRequests.some((pr) => sameRepo(pr.repoFullName, repoFullName) && sameLogin(pr.authorLogin, args.login) && isMaintainerAssociation(pr.authorAssociation)); + return { + repoFullName, + maintainerLane, + official: officialCounts, + cached, + effective: officialCounts ?? cached, + discrepancyReasons: reconciliationReasons(officialCounts, cached, maintainerLane), + freshness: { + officialUpdatedAt: args.profile.gittensor?.updatedAt ?? args.profile.gittensor?.evaluatedAt, + cachedLastActivityAt: cachedLastActivityAt(args.login, repoFullName, args.pullRequests, args.issues), + }, + }; + }); + const cachedTotals = sumReconciliationCounts(repos.map((repo) => repo.cached)); + const officialTotals = args.profile.gittensor + ? { + pullRequests: args.profile.gittensor.totals.pullRequests, + mergedPullRequests: args.profile.gittensor.totals.mergedPullRequests, + openPullRequests: args.profile.gittensor.totals.openPullRequests, + closedPullRequests: args.profile.gittensor.totals.closedPullRequests, + closedPullRequestRate: rate(args.profile.gittensor.totals.closedPullRequests, args.profile.gittensor.totals.pullRequests), + issues: args.profile.gittensor.totals.openIssues + args.profile.gittensor.totals.closedIssues, + openIssues: args.profile.gittensor.totals.openIssues, + closedIssues: args.profile.gittensor.totals.closedIssues, + solvedIssues: args.profile.gittensor.totals.solvedIssues, + validSolvedIssues: args.profile.gittensor.totals.validSolvedIssues, + credibility: args.profile.gittensor.credibility, + issueCredibility: args.profile.gittensor.issueCredibility, + } + : undefined; + const findings: SignalFinding[] = [ + ...(!officialTotals + ? [ + { + code: "official_source_unavailable", + severity: "warning" as const, + title: "Official contributor totals unavailable", + detail: "Cached GitHub history is context only until official contributor totals are available.", + }, + ] + : []), + ...repos + .filter((repo) => repo.maintainerLane) + .map((repo) => ({ + code: "maintainer_lane_context", + severity: "info" as const, + title: "Maintainer-lane history is separated", + detail: `${repo.repoFullName} is maintainer-associated context and should not inflate normal contributor evidence.`, + })), + ]; + return { + login: args.login, + generatedAt: nowIso(), + source: args.profile.source, + officialAuthoritative: Boolean(officialTotals), + totals: { official: officialTotals, cached: cachedTotals, effective: officialTotals ?? cachedTotals }, + repos, + findings, + summary: `${args.login} reconciliation: ${officialTotals ? "official totals authoritative" : "cached context only"}; ${repos.length} repo(s) compared.`, + }; +} + +function cachedReconciliationCounts( + login: string, + repoFullName: string, + pullRequests: PullRequestRecord[], + issues: IssueRecord[], + stat?: ContributorRepoStatRecord | undefined, +): ContributorOutcomeCounts { + const cachedPrs = pullRequests.filter((pr) => sameRepo(pr.repoFullName, repoFullName) && sameLogin(pr.authorLogin, login)); + const cachedIssues = issues.filter((issue) => sameRepo(issue.repoFullName, repoFullName) && sameLogin(issue.authorLogin, login)); + const mergedPullRequests = Math.max(cachedPrs.filter((pr) => pr.mergedAt || pr.state === "merged").length, stat?.mergedPullRequests ?? 0); + const openPullRequests = Math.max(cachedPrs.filter((pr) => pr.state === "open").length, stat?.openPullRequests ?? 0); + const pullRequestCount = Math.max(cachedPrs.length, stat?.pullRequests ?? 0); + const closedPullRequests = Math.max(cachedPrs.filter((pr) => pr.state === "closed").length, pullRequestCount - mergedPullRequests - openPullRequests, 0); + const openIssues = cachedIssues.filter((issue) => issue.state === "open").length; + const closedIssues = cachedIssues.filter((issue) => issue.state !== "open").length; + return { + pullRequests: pullRequestCount, + mergedPullRequests, + openPullRequests, + closedPullRequests, + issues: openIssues + closedIssues, + openIssues, + closedIssues, + solvedIssues: 0, + validSolvedIssues: 0, + }; +} + +function sumReconciliationCounts(counts: ContributorOutcomeCounts[]): ContributorOutcomeHistory["totals"] { + const summed = counts.reduce( + (acc, count) => ({ + pullRequests: acc.pullRequests + count.pullRequests, + mergedPullRequests: acc.mergedPullRequests + count.mergedPullRequests, + openPullRequests: acc.openPullRequests + count.openPullRequests, + closedPullRequests: acc.closedPullRequests + count.closedPullRequests, + issues: acc.issues + count.issues, + openIssues: acc.openIssues + count.openIssues, + closedIssues: acc.closedIssues + count.closedIssues, + solvedIssues: acc.solvedIssues + count.solvedIssues, + validSolvedIssues: acc.validSolvedIssues + count.validSolvedIssues, + }), + { pullRequests: 0, mergedPullRequests: 0, openPullRequests: 0, closedPullRequests: 0, issues: 0, openIssues: 0, closedIssues: 0, solvedIssues: 0, validSolvedIssues: 0 }, + ); + return { ...summed, closedPullRequestRate: rate(summed.closedPullRequests, summed.pullRequests), credibility: 0, issueCredibility: 0 }; +} + +function reconciliationReasons(official: ContributorOutcomeCounts | undefined, cached: ContributorOutcomeCounts, maintainerLane: boolean): string[] { + return [ + ...(!official ? ["Official source unavailable; cached GitHub history is context only."] : []), + ...(official && official.pullRequests !== cached.pullRequests + ? [`Official PR total ${official.pullRequests} differs from cached GitHub context ${cached.pullRequests}; official total is authoritative.`] + : []), + ...(official && official.openPullRequests < cached.openPullRequests ? ["Cached open PRs may include work outside the official lookback or not yet reflected upstream."] : []), + ...(official && official.closedPullRequests < cached.closedPullRequests ? ["Cached closed PRs may include stale or closed-unmerged context that official totals do not count the same way."] : []), + ...(maintainerLane ? ["Maintainer-owned repo history is separated from normal contributor evidence."] : []), + ]; +} + +function cachedLastActivityAt(login: string, repoFullName: string, pullRequests: PullRequestRecord[], issues: IssueRecord[]): string | undefined { + return [...pullRequests, ...issues] + .filter((item) => sameRepo(item.repoFullName, repoFullName) && sameLogin(item.authorLogin, login)) + .map((item) => item.updatedAt ?? item.createdAt) + .filter((value): value is string => Boolean(value)) + .sort() + .at(-1); +} + export function buildContributorPatternReport(history: ContributorOutcomeHistory, patternType: "success" | "failure"): ContributorPatternReport { const patterns = patternType === "success" ? history.successPatterns : history.failurePatterns; return { @@ -2541,6 +2741,10 @@ function sameLogin(value: string | null | undefined, login: string): boolean { return value?.toLowerCase() === login.toLowerCase(); } +function sameRepo(left: string | null | undefined, right: string | null | undefined): boolean { + return Boolean(left && right && left.toLowerCase() === right.toLowerCase()); +} + function topItems(items: string[], limit: number): string[] { const counts = new Map(); for (const item of items) counts.set(item, (counts.get(item) ?? 0) + 1); diff --git a/test/unit/signals-v2.test.ts b/test/unit/signals-v2.test.ts index 45a3850b0a..82ea1145de 100644 --- a/test/unit/signals-v2.test.ts +++ b/test/unit/signals-v2.test.ts @@ -635,6 +635,14 @@ describe("v2 signal builders", () => { expect(role).toMatchObject({ role: "owner", maintainerLane: true, normalContributorEvidenceAllowed: false }); expect(history.repoOutcomes.filter((outcome) => outcome.repoFullName.toLowerCase() === "jsonbored/awesome-claude")).toHaveLength(1); expect(history.repoOutcomes.find((outcome) => outcome.repoFullName === "jsonbored/awesome-claude")).toMatchObject({ successLevel: "maintainer_context" }); + expect(history.reconciliation).toMatchObject({ officialAuthoritative: true, totals: { effective: { pullRequests: 63, mergedPullRequests: 46 } } }); + expect(history.reconciliation?.repos.find((entry) => entry.repoFullName.toLowerCase() === "jsonbored/awesome-claude")).toMatchObject({ + maintainerLane: true, + discrepancyReasons: expect.arrayContaining([ + expect.stringContaining("Official PR total"), + expect.stringContaining("Maintainer-owned repo history"), + ]), + }); expect(buildContributorPatternReport(history, "failure").patterns.map((pattern) => pattern.title)).toContain("Raw issue activity is not solved discovery evidence"); expect(strategy.maintainerLaneRepos).toEqual(expect.arrayContaining([expect.objectContaining({ repoFullName: "jsonbored/awesome-claude" })])); expect(recommendation.recommendation).toBe("maintainer_lane"); @@ -645,6 +653,22 @@ describe("v2 signal builders", () => { expect(JSON.stringify({ strategy, review })).not.toMatch(/wallet|farming|reward/i); }); + it("labels GitHub-only contributor reconciliation as context", () => { + const profile = buildContributorProfile("oktofeesh1", { login: "oktofeesh1", topLanguages: ["TypeScript"], source: "github" }, pullRequests, issues); + const history = buildContributorOutcomeHistory({ + login: "oktofeesh1", + profile, + repositories: [repo], + pullRequests, + issues, + repoStats: [{ login: "oktofeesh1", repoFullName: repo.fullName, pullRequests: 2, mergedPullRequests: 1, openPullRequests: 1, issues: 1, stalePullRequests: 0, unlinkedPullRequests: 0, dominantLabels: ["feature"], lastActivityAt: "2026-05-30T00:00:00.000Z" }], + }); + + expect(history.reconciliation).toMatchObject({ officialAuthoritative: false, source: "github_cache" }); + expect(history.reconciliation?.findings).toEqual(expect.arrayContaining([expect.objectContaining({ code: "official_source_unavailable" })])); + expect(history.reconciliation?.repos[0]?.discrepancyReasons).toEqual(expect.arrayContaining([expect.stringContaining("Official source unavailable")])); + }); + it("classifies role context from GitHub associations, official activity, cache activity, and unknown state", () => { const memberPr: PullRequestRecord = { ...pullRequests[0]!, From 0cd041b3122aac213787f2863671ff086e5b2a53 Mon Sep 17 00:00:00 2001 From: oktofeesh1 <287075021+oktofeesh1@users.noreply.github.com> Date: Fri, 29 May 2026 22:29:51 -0700 Subject: [PATCH 2/4] test(readiness): keep freshness fixtures current --- test/integration/api.test.ts | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/test/integration/api.test.ts b/test/integration/api.test.ts index cb45405b5d..de4f64d520 100644 --- a/test/integration/api.test.ts +++ b/test/integration/api.test.ts @@ -1140,7 +1140,7 @@ describe("api routes", () => { fetchedCount: 2, expectedCount: 2, pageCount: 1, - completedAt: "2026-05-23T00:00:00.000Z", + completedAt: new Date().toISOString(), warnings: [], }); const refreshingReadiness = await app.request("/v1/readiness", { headers: apiHeaders(refreshingEnv) }, refreshingEnv); @@ -2351,6 +2351,9 @@ async function mcpJson(response: Response): Promise { } async function seedSignalData(env: Env): Promise { + const freshAt = new Date().toISOString(); + const previousFreshAt = new Date(Date.now() - 60_000).toISOString(); + await upsertInstallation(env, { installation: { id: 123, @@ -2371,7 +2374,7 @@ async function seedSignalData(env: Env): Promise { missingEvents: [], permissions: { metadata: "read", pull_requests: "read", issues: "write" }, events: ["issues", "pull_request", "repository"], - checkedAt: "2026-05-23T00:00:00.000Z", + checkedAt: freshAt, }); const snapshot = normalizeRegistryPayload( { @@ -2384,7 +2387,7 @@ async function seedSignalData(env: Env): Promise { }, }, { kind: "raw-github", url: "https://example.test/master_repositories.json" }, - "2026-05-23T00:00:00.000Z", + freshAt, ); await persistRegistrySnapshot( env, @@ -2399,7 +2402,7 @@ async function seedSignalData(env: Env): Promise { }, }, { kind: "raw-github", url: "https://example.test/old_master_repositories.json" }, - "2026-05-22T00:00:00.000Z", + previousFreshAt, ), ); await persistRegistrySnapshot(env, snapshot); @@ -2414,7 +2417,7 @@ async function seedSignalData(env: Env): Promise { id: "scoring-1", sourceKind: "test", sourceUrl: "fixture://scoring", - fetchedAt: "2026-05-23T00:00:00.000Z", + fetchedAt: freshAt, activeModel: "current_density_model", constants: { OSS_EMISSION_SHARE: 0.9, @@ -2459,7 +2462,7 @@ async function seedSignalData(env: Env): Promise { closedUnmergedPullRequestsTotal: 0, labelsTotal: 2, sourceKind: "github", - fetchedAt: "2026-05-23T00:00:00.000Z", + fetchedAt: freshAt, payload: {}, }); await Promise.all( @@ -2482,7 +2485,7 @@ async function seedSignalData(env: Env): Promise { fetchedCount: record.fetchedCount, expectedCount: record.expectedCount, pageCount: 1, - completedAt: "2026-05-23T00:00:00.000Z", + completedAt: freshAt, warnings: [], }), ), @@ -2516,7 +2519,7 @@ async function seedSignalData(env: Env): Promise { missingEvents: [], permissions: { metadata: "read", pull_requests: "read", issues: "write" }, events: ["issues", "issue_comment", "pull_request", "repository"], - checkedAt: "2026-05-23T00:00:00.000Z", + checkedAt: freshAt, }); await upsertIssueFromGitHub(env, "entrius/allways-ui", { number: 7, @@ -2552,10 +2555,10 @@ async function seedSignalData(env: Env): Promise { repoFullName: "entrius/allways-ui", pullNumber: 12, status: "complete", - filesSyncedAt: "2026-05-23T00:00:00.000Z", - reviewsSyncedAt: "2026-05-23T00:00:00.000Z", - checksSyncedAt: "2026-05-23T00:00:00.000Z", - lastSyncedAt: "2026-05-23T00:00:00.000Z", + filesSyncedAt: freshAt, + reviewsSyncedAt: freshAt, + checksSyncedAt: freshAt, + lastSyncedAt: freshAt, }); await upsertPullRequestFile(env, { repoFullName: "entrius/allways-ui", @@ -2600,10 +2603,10 @@ async function seedSignalData(env: Env): Promise { repoFullName: "entrius/allways-ui", pullNumber: 13, status: "complete", - filesSyncedAt: "2026-05-23T00:00:00.000Z", - reviewsSyncedAt: "2026-05-23T00:00:00.000Z", - checksSyncedAt: "2026-05-23T00:00:00.000Z", - lastSyncedAt: "2026-05-23T00:00:00.000Z", + filesSyncedAt: freshAt, + reviewsSyncedAt: freshAt, + checksSyncedAt: freshAt, + lastSyncedAt: freshAt, }); await upsertRecentMergedPullRequest(env, { repoFullName: "entrius/allways-ui", From c204f2a4b7e385febeb4a4425e9f54691eb78821 Mon Sep 17 00:00:00 2001 From: oktofeesh1 <287075021+oktofeesh1@users.noreply.github.com> Date: Sat, 30 May 2026 00:40:28 -0700 Subject: [PATCH 3/4] fix(data): reconcile official and cached contributor history --- src/api/routes.ts | 1 + src/mcp/server.ts | 1 + src/queue/processors.ts | 2 +- src/services/agent-orchestrator.ts | 2 +- src/services/decision-pack.ts | 1 + src/signals/engine.ts | 63 ++++++++++++----- test/unit/signals-v2.test.ts | 109 +++++++++++++++++++++++++++++ 7 files changed, 159 insertions(+), 20 deletions(-) diff --git a/src/api/routes.ts b/src/api/routes.ts index f14f12ac4a..a42f429baf 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -1296,6 +1296,7 @@ async function loadContributorFastContext(env: Env, login: string) { pullRequests: contributorPullRequests, issues: contributorIssues, repoStats, + cachedRepoStats, }); return { login, diff --git a/src/mcp/server.ts b/src/mcp/server.ts index bcf3164680..2ba2370e54 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -877,6 +877,7 @@ export class GittensoryMcp { pullRequests: contributorPullRequests, issues: contributorIssues, repoStats, + cachedRepoStats, }); return { profile, diff --git a/src/queue/processors.ts b/src/queue/processors.ts index cb951d4f94..c762660602 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -298,7 +298,7 @@ async function buildContributorEvidence(env: Env, login?: string): Promise const profile = buildContributorProfile(contributorLogin, github, contributorPullRequests, contributorIssues, repoStats, gittensorSnapshot); const fit = buildContributorFit(profile, repositories, allIssues, allPullRequests, syncStates, repoStats, issueQualityByRepo); const scoringProfile = buildContributorScoringProfile({ login: contributorLogin, fit, scoringSnapshot: snapshot }); - const outcomeHistory = buildContributorOutcomeHistory({ login: contributorLogin, profile, repositories, pullRequests: allPullRequests, issues: allIssues, repoStats }); + const outcomeHistory = buildContributorOutcomeHistory({ login: contributorLogin, profile, repositories, pullRequests: allPullRequests, issues: allIssues, repoStats, cachedRepoStats }); const strategy = buildContributorStrategy({ login: contributorLogin, fit, scoringProfile, scoringSnapshot: snapshot, outcomeHistory }); const evidence: ContributorEvidenceRecord = { login: contributorLogin, diff --git a/src/services/agent-orchestrator.ts b/src/services/agent-orchestrator.ts index 7c3deaf62d..38db1a0849 100644 --- a/src/services/agent-orchestrator.ts +++ b/src/services/agent-orchestrator.ts @@ -301,7 +301,7 @@ async function analyzeLocalBranch(env: Env, input: LocalBranchAnalysisInput): Pr ]); const repoStats = contributorRepoStatsFromGittensor(gittensorSnapshot).length > 0 ? contributorRepoStatsFromGittensor(gittensorSnapshot) : cachedRepoStats; const profile = buildContributorProfile(input.login, github, contributorPullRequests, contributorIssues, repoStats, gittensorSnapshot); - const outcomeHistory = buildContributorOutcomeHistory({ login: input.login, profile, repositories, pullRequests: contributorPullRequests, issues: contributorIssues, repoStats }); + const outcomeHistory = buildContributorOutcomeHistory({ login: input.login, profile, repositories, pullRequests: contributorPullRequests, issues: contributorIssues, repoStats, cachedRepoStats }); const fit = buildContributorFit(profile, repositories, [], [], syncStates, repoStats); const scoringProfile = buildContributorScoringProfile({ login: input.login, fit, scoringSnapshot }); return buildLocalBranchAnalysis({ diff --git a/src/services/decision-pack.ts b/src/services/decision-pack.ts index dbc783f410..87967ce3f2 100644 --- a/src/services/decision-pack.ts +++ b/src/services/decision-pack.ts @@ -262,6 +262,7 @@ export async function buildAndPersistContributorDecisionPack(env: Env, login: st pullRequests: contributorPullRequests, issues: contributorIssues, repoStats, + cachedRepoStats, }); const fit = buildContributorFit(profile, repositories, [], [], syncStates, repoStats); const scoringProfile = buildContributorScoringProfile({ login, fit, scoringSnapshot }); diff --git a/src/signals/engine.ts b/src/signals/engine.ts index 4c59bbfeb7..f52d7822d0 100644 --- a/src/signals/engine.ts +++ b/src/signals/engine.ts @@ -1304,6 +1304,7 @@ export function buildContributorOutcomeHistory(args: { pullRequests: PullRequestRecord[]; issues: IssueRecord[]; repoStats: ContributorRepoStatRecord[]; + cachedRepoStats?: ContributorRepoStatRecord[] | undefined; }): ContributorOutcomeHistory { const repoByName = new Map(args.repositories.map((repo) => [repo.fullName.toLowerCase(), repo])); const repoNamesByKey = new Map(); @@ -1327,12 +1328,12 @@ export function buildContributorOutcomeHistory(args: { const repo = repoByName.get(repoFullName.toLowerCase()) ?? null; const official = officialByRepo.get(repoFullName.toLowerCase()); const cachedStat = statsByRepo.get(repoFullName.toLowerCase()); - const cachedPrs = args.pullRequests.filter((pr) => pr.repoFullName === repoFullName && sameLogin(pr.authorLogin, args.login)); - const cachedIssues = args.issues.filter((issue) => issue.repoFullName === repoFullName && sameLogin(issue.authorLogin, args.login)); + const cachedPrs = args.pullRequests.filter((pr) => sameRepo(pr.repoFullName, repoFullName) && sameLogin(pr.authorLogin, args.login)); + const cachedIssues = args.issues.filter((issue) => sameRepo(issue.repoFullName, repoFullName) && sameLogin(issue.authorLogin, args.login)); const pullRequests = official?.pullRequests ?? Math.max(cachedPrs.length, cachedStat?.pullRequests ?? 0); 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").length, pullRequests - mergedPullRequests - 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 solvedIssues = official?.solvedIssues ?? 0; @@ -1424,17 +1425,25 @@ export function buildContributorReconciliationReport(args: { pullRequests: PullRequestRecord[]; issues: IssueRecord[]; repoStats: ContributorRepoStatRecord[]; + cachedRepoStats?: ContributorRepoStatRecord[] | undefined; history?: ContributorOutcomeHistory | undefined; }): ContributorReconciliationReport { - const repoNames = new Set(); - for (const repo of args.profile.gittensor?.repositories ?? []) repoNames.add(repo.repoFullName); - for (const stat of args.repoStats.filter((stat) => sameLogin(stat.login, args.login))) repoNames.add(stat.repoFullName); - for (const pr of args.pullRequests.filter((pr) => sameLogin(pr.authorLogin, args.login))) repoNames.add(pr.repoFullName); - for (const issue of args.issues.filter((issue) => sameLogin(issue.authorLogin, args.login))) repoNames.add(issue.repoFullName); + const cachedStats = args.cachedRepoStats ?? args.repoStats; + const repoNamesByKey = new Map(); + const addRepoName = (repoFullName: string, priority: number) => { + const key = repoFullName.toLowerCase(); + const current = repoNamesByKey.get(key); + if (!current || priority >= current.priority) repoNamesByKey.set(key, { repoFullName, priority }); + }; + for (const repoFullName of args.profile.registeredRepoActivity.reposTouched) addRepoName(repoFullName, 1); + for (const stat of cachedStats.filter((stat) => sameLogin(stat.login, args.login))) addRepoName(stat.repoFullName, 2); + for (const pr of args.pullRequests.filter((pr) => sameLogin(pr.authorLogin, args.login))) addRepoName(pr.repoFullName, 3); + for (const issue of args.issues.filter((issue) => sameLogin(issue.authorLogin, args.login))) addRepoName(issue.repoFullName, 3); + for (const repo of args.profile.gittensor?.repositories ?? []) addRepoName(repo.repoFullName, 4); const officialByRepo = new Map(args.profile.gittensor?.repositories.map((repo) => [repo.repoFullName.toLowerCase(), repo]) ?? []); - const statByRepo = new Map(args.repoStats.filter((stat) => sameLogin(stat.login, args.login)).map((stat) => [stat.repoFullName.toLowerCase(), stat])); + const statByRepo = new Map(cachedStats.filter((stat) => sameLogin(stat.login, args.login)).map((stat) => [stat.repoFullName.toLowerCase(), stat])); const repoByName = new Map(args.repositories.map((repo) => [repo.fullName.toLowerCase(), repo])); - const repos = [...repoNames].sort((left, right) => left.localeCompare(right)).map((repoFullName) => { + const repos = [...repoNamesByKey.values()].map((entry) => entry.repoFullName).sort((left, right) => left.localeCompare(right)).map((repoFullName) => { const key = repoFullName.toLowerCase(); const official = officialByRepo.get(key); const cached = cachedReconciliationCounts(args.login, repoFullName, args.pullRequests, args.issues, statByRepo.get(key)); @@ -1451,9 +1460,13 @@ export function buildContributorReconciliationReport(args: { validSolvedIssues: official.validSolvedIssues, } : undefined; + const repo = repoByName.get(key); + const [repoOwner] = repoFullName.split("/"); const maintainerLane = - sameLogin(repoByName.get(key)?.owner, args.login) || - args.pullRequests.some((pr) => sameRepo(pr.repoFullName, repoFullName) && sameLogin(pr.authorLogin, args.login) && isMaintainerAssociation(pr.authorAssociation)); + sameLogin(repo?.owner, args.login) || + sameLogin(repoOwner, args.login) || + args.pullRequests.some((pr) => sameRepo(pr.repoFullName, repoFullName) && sameLogin(pr.authorLogin, args.login) && isMaintainerAssociation(pr.authorAssociation)) || + args.issues.some((issue) => sameRepo(issue.repoFullName, repoFullName) && sameLogin(issue.authorLogin, args.login) && isMaintainerAssociation(issue.authorAssociation)); return { repoFullName, maintainerLane, @@ -1528,15 +1541,19 @@ function cachedReconciliationCounts( const mergedPullRequests = Math.max(cachedPrs.filter((pr) => pr.mergedAt || pr.state === "merged").length, stat?.mergedPullRequests ?? 0); const openPullRequests = Math.max(cachedPrs.filter((pr) => pr.state === "open").length, stat?.openPullRequests ?? 0); const pullRequestCount = Math.max(cachedPrs.length, stat?.pullRequests ?? 0); - const closedPullRequests = Math.max(cachedPrs.filter((pr) => pr.state === "closed").length, pullRequestCount - mergedPullRequests - openPullRequests, 0); - const openIssues = cachedIssues.filter((issue) => issue.state === "open").length; - const closedIssues = cachedIssues.filter((issue) => issue.state !== "open").length; + const closedUnmergedPullRequests = cachedPrs.filter((pr) => pr.state === "closed" && !pr.mergedAt).length; + const closedPullRequests = Math.max(closedUnmergedPullRequests, pullRequestCount - mergedPullRequests - openPullRequests, 0); + const openIssueRows = cachedIssues.filter((issue) => issue.state === "open").length; + const closedIssueRows = cachedIssues.filter((issue) => issue.state !== "open").length; + const issueCount = Math.max(cachedIssues.length, stat?.issues ?? 0); + const openIssues = openIssueRows; + const closedIssues = Math.max(closedIssueRows, issueCount - openIssues, 0); return { pullRequests: pullRequestCount, mergedPullRequests, openPullRequests, closedPullRequests, - issues: openIssues + closedIssues, + issues: issueCount, openIssues, closedIssues, solvedIssues: 0, @@ -1568,8 +1585,18 @@ function reconciliationReasons(official: ContributorOutcomeCounts | undefined, c ...(official && official.pullRequests !== cached.pullRequests ? [`Official PR total ${official.pullRequests} differs from cached GitHub context ${cached.pullRequests}; official total is authoritative.`] : []), - ...(official && official.openPullRequests < cached.openPullRequests ? ["Cached open PRs may include work outside the official lookback or not yet reflected upstream."] : []), - ...(official && official.closedPullRequests < cached.closedPullRequests ? ["Cached closed PRs may include stale or closed-unmerged context that official totals do not count the same way."] : []), + ...(official && official.mergedPullRequests !== cached.mergedPullRequests + ? [`Official merged PR total ${official.mergedPullRequests} differs from cached GitHub context ${cached.mergedPullRequests}; official merge data is authoritative.`] + : []), + ...(official && official.openPullRequests !== cached.openPullRequests ? ["Official open PR count differs from cached GitHub context; refresh timing or lookback windows may differ."] : []), + ...(official && official.closedPullRequests !== cached.closedPullRequests ? ["Official closed PR count differs from cached closed-unmerged context."] : []), + ...(official && official.issues !== cached.issues + ? [`Official issue total ${official.issues} differs from cached GitHub context ${cached.issues}; official issue data is authoritative.`] + : []), + ...(official && official.openIssues !== cached.openIssues ? ["Official open issue count differs from cached GitHub context."] : []), + ...(official && official.closedIssues !== cached.closedIssues ? ["Official closed issue count differs from cached GitHub context."] : []), + ...(official && official.solvedIssues !== cached.solvedIssues ? ["Official solved issue count differs from cached solver context."] : []), + ...(official && official.validSolvedIssues !== cached.validSolvedIssues ? ["Official valid-solved issue count differs from cached solver context."] : []), ...(maintainerLane ? ["Maintainer-owned repo history is separated from normal contributor evidence."] : []), ]; } diff --git a/test/unit/signals-v2.test.ts b/test/unit/signals-v2.test.ts index 82ea1145de..b14b677361 100644 --- a/test/unit/signals-v2.test.ts +++ b/test/unit/signals-v2.test.ts @@ -669,6 +669,115 @@ describe("v2 signal builders", () => { expect(history.reconciliation?.repos[0]?.discrepancyReasons).toEqual(expect.arrayContaining([expect.stringContaining("Official source unavailable")])); }); + it("keeps cached reconciliation stats separate from official profile counts", () => { + const profile = buildContributorProfile( + "jsonbored", + { login: "JSONbored", topLanguages: ["TypeScript"], source: "github" }, + [], + [], + [ + { + login: "jsonbored", + repoFullName: "JSONbored/awesome-claude", + pullRequests: 10, + mergedPullRequests: 8, + openPullRequests: 1, + issues: 5, + stalePullRequests: 0, + unlinkedPullRequests: 0, + dominantLabels: ["feature"], + }, + ], + { + source: "gittensor_api", + githubId: "49853598", + githubUsername: "JSONbored", + isEligible: true, + credibility: 1, + eligibleRepoCount: 1, + issueDiscoveryScore: 0, + issueTokenScore: 0, + issueCredibility: 1, + isIssueEligible: true, + issueEligibleRepoCount: 1, + alphaPerDay: 0, + taoPerDay: 0, + usdPerDay: 0, + totals: { pullRequests: 10, mergedPullRequests: 8, openPullRequests: 1, closedPullRequests: 1, openIssues: 3, closedIssues: 2, solvedIssues: 1, validSolvedIssues: 1 }, + repositories: [ + { + repoFullName: "JSONbored/awesome-claude", + pullRequests: 10, + mergedPullRequests: 8, + openPullRequests: 1, + closedPullRequests: 1, + openIssues: 3, + closedIssues: 2, + solvedIssues: 1, + validSolvedIssues: 1, + isEligible: true, + isIssueEligible: true, + credibility: 1, + issueCredibility: 1, + totalScore: 10, + baseTotalScore: 10, + }, + ], + pullRequests: [], + issueLabels: ["feature"], + }, + ); + const officialStats: ContributorRepoStatRecord[] = [ + { login: "jsonbored", repoFullName: "JSONbored/awesome-claude", pullRequests: 10, mergedPullRequests: 8, openPullRequests: 1, issues: 5, stalePullRequests: 0, unlinkedPullRequests: 0, dominantLabels: ["feature"] }, + ]; + const cachedRepoStats: ContributorRepoStatRecord[] = [ + { login: "jsonbored", repoFullName: "jsonbored/Awesome-Claude", pullRequests: 2, mergedPullRequests: 1, openPullRequests: 1, issues: 4, stalePullRequests: 0, unlinkedPullRequests: 0, dominantLabels: ["bug"] }, + ]; + const history = buildContributorOutcomeHistory({ + login: "jsonbored", + profile, + repositories: [], + pullRequests: [ + { ...pullRequests[0]!, repoFullName: "JSONbored/awesome-claude", number: 1, authorLogin: "jsonbored", state: "closed", mergedAt: "2026-05-01T00:00:00.000Z" }, + { ...pullRequests[0]!, repoFullName: "jsonbored/Awesome-Claude", number: 2, authorLogin: "jsonbored", state: "open", mergedAt: null }, + ], + issues: [{ ...issues[0]!, repoFullName: "JSONbored/awesome-claude", number: 3, authorLogin: "jsonbored", authorAssociation: "OWNER", state: "open" }], + repoStats: officialStats, + cachedRepoStats, + }); + + const matchingRepos = history.reconciliation?.repos.filter((entry) => entry.repoFullName.toLowerCase() === "jsonbored/awesome-claude") ?? []; + expect(matchingRepos).toHaveLength(1); + expect(matchingRepos[0]).toMatchObject({ + maintainerLane: true, + official: { pullRequests: 10, mergedPullRequests: 8, openPullRequests: 1, closedPullRequests: 1, issues: 5, openIssues: 3, closedIssues: 2, solvedIssues: 1, validSolvedIssues: 1 }, + cached: { pullRequests: 2, mergedPullRequests: 1, openPullRequests: 1, closedPullRequests: 0, issues: 4, openIssues: 1, closedIssues: 3 }, + }); + expect(matchingRepos[0]?.discrepancyReasons).toEqual( + expect.arrayContaining([ + expect.stringContaining("Official PR total"), + expect.stringContaining("Official merged PR total"), + expect.stringContaining("Official issue total"), + expect.stringContaining("Official open issue count"), + expect.stringContaining("Official valid-solved issue count"), + expect.stringContaining("Maintainer-owned repo history"), + ]), + ); + expect(history.reconciliation?.totals.cached).toMatchObject({ pullRequests: 2, mergedPullRequests: 1, openPullRequests: 1, closedPullRequests: 0, issues: 4, openIssues: 1, closedIssues: 3 }); + }); + + it("uses cached issue associations for reconciliation maintainer lanes", () => { + const issueOnly: IssueRecord = { repoFullName: "entrius/allways", number: 88, title: "Maintainer filed issue", state: "open", authorLogin: "memberdev", authorAssociation: "MEMBER", labels: ["bug"], linkedPrs: [] }; + const profile = buildContributorProfile("memberdev", { login: "memberdev", topLanguages: ["TypeScript"], source: "github" }, [], [issueOnly]); + const history = buildContributorOutcomeHistory({ login: "memberdev", profile, repositories: [], pullRequests: [], issues: [issueOnly], repoStats: [] }); + + expect(history.reconciliation?.repos[0]).toMatchObject({ + repoFullName: "entrius/allways", + maintainerLane: true, + discrepancyReasons: expect.arrayContaining([expect.stringContaining("Maintainer-owned repo history")]), + }); + }); + it("classifies role context from GitHub associations, official activity, cache activity, and unknown state", () => { const memberPr: PullRequestRecord = { ...pullRequests[0]!, From 677351d6e41236375f96e94950ee55e9f2dd88d3 Mon Sep 17 00:00:00 2001 From: oktofeesh1 <287075021+oktofeesh1@users.noreply.github.com> Date: Sat, 30 May 2026 01:39:05 -0700 Subject: [PATCH 4/4] fix(data): publish authoritative reconciliation semantics --- src/openapi/schemas.ts | 47 ++++++++++++++++++++++++++++++++++++ src/signals/engine.ts | 14 ++++++++--- test/unit/openapi.test.ts | 1 + test/unit/signals-v2.test.ts | 10 +++++++- 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/src/openapi/schemas.ts b/src/openapi/schemas.ts index 0eccc0eadc..cf5efc4f5c 100644 --- a/src/openapi/schemas.ts +++ b/src/openapi/schemas.ts @@ -945,11 +945,58 @@ export const RoleContextSchema = z }) .openapi("RoleContext"); +const ContributorOutcomeCountsSchema = z.object({ + pullRequests: z.number(), + mergedPullRequests: z.number(), + openPullRequests: z.number(), + closedPullRequests: z.number(), + issues: z.number(), + openIssues: z.number(), + closedIssues: z.number(), + solvedIssues: z.number(), + validSolvedIssues: z.number(), +}); + +const ContributorOutcomeTotalsSchema = ContributorOutcomeCountsSchema.extend({ + closedPullRequestRate: z.number(), + credibility: z.number(), + issueCredibility: z.number(), +}); + +const ContributorReconciliationReportSchema = z.object({ + login: z.string(), + generatedAt: z.string(), + source: z.enum(["gittensor_api", "github_cache"]), + officialAuthoritative: z.boolean(), + totals: z.object({ + official: ContributorOutcomeTotalsSchema.optional(), + cached: ContributorOutcomeTotalsSchema, + effective: ContributorOutcomeTotalsSchema, + }), + repos: z.array( + z.object({ + repoFullName: z.string(), + maintainerLane: z.boolean(), + official: ContributorOutcomeCountsSchema.optional(), + cached: ContributorOutcomeCountsSchema, + effective: ContributorOutcomeCountsSchema, + discrepancyReasons: z.array(z.string()), + freshness: z.object({ + officialUpdatedAt: z.string().optional(), + cachedLastActivityAt: z.string().optional(), + }), + }), + ), + findings: z.array(FindingSchema), + summary: z.string(), +}); + export const ContributorOutcomeHistorySchema = z .object({ login: z.string(), generatedAt: z.string(), source: z.enum(["gittensor_api", "github_cache"]), + reconciliation: ContributorReconciliationReportSchema.optional(), totals: z.record(z.number()), repoOutcomes: z.array(z.record(z.unknown())), successPatterns: z.array(z.record(z.unknown())), diff --git a/src/signals/engine.ts b/src/signals/engine.ts index f52d7822d0..90ab87ea2a 100644 --- a/src/signals/engine.ts +++ b/src/signals/engine.ts @@ -1443,6 +1443,7 @@ export function buildContributorReconciliationReport(args: { const officialByRepo = new Map(args.profile.gittensor?.repositories.map((repo) => [repo.repoFullName.toLowerCase(), repo]) ?? []); const statByRepo = new Map(cachedStats.filter((stat) => sameLogin(stat.login, args.login)).map((stat) => [stat.repoFullName.toLowerCase(), stat])); const repoByName = new Map(args.repositories.map((repo) => [repo.fullName.toLowerCase(), repo])); + const officialAuthoritative = Boolean(args.profile.gittensor); const repos = [...repoNamesByKey.values()].map((entry) => entry.repoFullName).sort((left, right) => left.localeCompare(right)).map((repoFullName) => { const key = repoFullName.toLowerCase(); const official = officialByRepo.get(key); @@ -1472,8 +1473,8 @@ export function buildContributorReconciliationReport(args: { maintainerLane, official: officialCounts, cached, - effective: officialCounts ?? cached, - discrepancyReasons: reconciliationReasons(officialCounts, cached, maintainerLane), + effective: officialCounts ?? (officialAuthoritative ? emptyOutcomeCounts() : cached), + discrepancyReasons: reconciliationReasons(officialCounts, cached, maintainerLane, officialAuthoritative), freshness: { officialUpdatedAt: args.profile.gittensor?.updatedAt ?? args.profile.gittensor?.evaluatedAt, cachedLastActivityAt: cachedLastActivityAt(args.login, repoFullName, args.pullRequests, args.issues), @@ -1579,9 +1580,14 @@ function sumReconciliationCounts(counts: ContributorOutcomeCounts[]): Contributo return { ...summed, closedPullRequestRate: rate(summed.closedPullRequests, summed.pullRequests), credibility: 0, issueCredibility: 0 }; } -function reconciliationReasons(official: ContributorOutcomeCounts | undefined, cached: ContributorOutcomeCounts, maintainerLane: boolean): string[] { +function emptyOutcomeCounts(): ContributorOutcomeCounts { + return { pullRequests: 0, mergedPullRequests: 0, openPullRequests: 0, closedPullRequests: 0, issues: 0, openIssues: 0, closedIssues: 0, solvedIssues: 0, validSolvedIssues: 0 }; +} + +function reconciliationReasons(official: ContributorOutcomeCounts | undefined, cached: ContributorOutcomeCounts, maintainerLane: boolean, officialAuthoritative: boolean): string[] { return [ - ...(!official ? ["Official source unavailable; cached GitHub history is context only."] : []), + ...(!official && officialAuthoritative && cached.pullRequests + cached.issues > 0 ? ["Official source omits this repo; cached GitHub history is context only."] : []), + ...(!official && !officialAuthoritative ? ["Official source unavailable; cached GitHub history is context only."] : []), ...(official && official.pullRequests !== cached.pullRequests ? [`Official PR total ${official.pullRequests} differs from cached GitHub context ${cached.pullRequests}; official total is authoritative.`] : []), diff --git a/test/unit/openapi.test.ts b/test/unit/openapi.test.ts index ffc3e377a2..be0b8689fc 100644 --- a/test/unit/openapi.test.ts +++ b/test/unit/openapi.test.ts @@ -74,6 +74,7 @@ describe("OpenAPI contract", () => { expect(spec.components?.schemas?.AgentAction).toBeDefined(); expect(JSON.stringify(spec.components?.schemas?.ScorePreviewResult)).toContain("scenarioPreviews"); expect(JSON.stringify(spec.components?.schemas?.RepoIntelligence)).toContain("burdenForecastFreshness"); + expect(JSON.stringify(spec.components?.schemas?.ContributorOutcomeHistory)).toContain("reconciliation"); expect(JSON.stringify(spec.components?.schemas?.LocalBranchAnalysis)).toContain("baseFreshness"); expect(JSON.stringify(spec.components?.schemas?.LocalBranchAnalysis)).toContain("recommendedRerunCondition"); }); diff --git a/test/unit/signals-v2.test.ts b/test/unit/signals-v2.test.ts index b14b677361..cd0480d78e 100644 --- a/test/unit/signals-v2.test.ts +++ b/test/unit/signals-v2.test.ts @@ -732,6 +732,7 @@ describe("v2 signal builders", () => { ]; const cachedRepoStats: ContributorRepoStatRecord[] = [ { login: "jsonbored", repoFullName: "jsonbored/Awesome-Claude", pullRequests: 2, mergedPullRequests: 1, openPullRequests: 1, issues: 4, stalePullRequests: 0, unlinkedPullRequests: 0, dominantLabels: ["bug"] }, + { login: "jsonbored", repoFullName: "entrius/gittensor", pullRequests: 1, mergedPullRequests: 0, openPullRequests: 1, issues: 0, stalePullRequests: 0, unlinkedPullRequests: 0, dominantLabels: ["bug"] }, ]; const history = buildContributorOutcomeHistory({ login: "jsonbored", @@ -740,6 +741,7 @@ describe("v2 signal builders", () => { pullRequests: [ { ...pullRequests[0]!, repoFullName: "JSONbored/awesome-claude", number: 1, authorLogin: "jsonbored", state: "closed", mergedAt: "2026-05-01T00:00:00.000Z" }, { ...pullRequests[0]!, repoFullName: "jsonbored/Awesome-Claude", number: 2, authorLogin: "jsonbored", state: "open", mergedAt: null }, + { ...pullRequests[0]!, repoFullName: "entrius/gittensor", number: 3, authorLogin: "jsonbored", state: "open", mergedAt: null }, ], issues: [{ ...issues[0]!, repoFullName: "JSONbored/awesome-claude", number: 3, authorLogin: "jsonbored", authorAssociation: "OWNER", state: "open" }], repoStats: officialStats, @@ -763,7 +765,13 @@ describe("v2 signal builders", () => { expect.stringContaining("Maintainer-owned repo history"), ]), ); - expect(history.reconciliation?.totals.cached).toMatchObject({ pullRequests: 2, mergedPullRequests: 1, openPullRequests: 1, closedPullRequests: 0, issues: 4, openIssues: 1, closedIssues: 3 }); + expect(history.reconciliation?.totals.cached).toMatchObject({ pullRequests: 3, mergedPullRequests: 1, openPullRequests: 2, closedPullRequests: 0, issues: 4, openIssues: 1, closedIssues: 3 }); + expect(history.reconciliation?.repos.find((entry) => entry.repoFullName === "entrius/gittensor")).toMatchObject({ + official: undefined, + cached: { pullRequests: 1, openPullRequests: 1 }, + effective: { pullRequests: 0, openPullRequests: 0 }, + discrepancyReasons: expect.arrayContaining([expect.stringContaining("Official source omits this repo")]), + }); }); it("uses cached issue associations for reconciliation maintainer lanes", () => {