From e817ae284352fb53d94eddf1b4c00e775421070b Mon Sep 17 00:00:00 2001 From: glorydavid03023 Date: Thu, 2 Jul 2026 04:44:03 -0500 Subject: [PATCH] fix(decision): match label history against config labels case-insensitively buildContributorDecisionPack built its label-history Set straight from registeredRepoActivity.dominantLabels (raw GitHub casing) and looked up config labels with a raw `.has(label)`. The opportunity engine (signals/engine.ts) lowercases both sides, and this file's own languageSet is already case-insensitive, so labels were the local outlier: a mixed-case dominant label (e.g. "Good first issue") never overlapped a differently-cased config label (e.g. "good first issue"), silently dropping it from the decision-pack label-overlap copy. Lowercase both the built Set and the config-key lookup so label overlap is case-insensitive, matching the engine and the languageSet handling. Adds a regression test covering a mixed-case dominant label end-to-end and a mixed-case config key on the lookup side. No issue because issue creation is restricted on this repo; this aligns a duplicated case-insensitive-label-overlap concept with its canonical sibling, no schema or API change. --- src/services/decision-pack.ts | 6 ++++-- test/unit/decision-pack.test.ts | 37 +++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/services/decision-pack.ts b/src/services/decision-pack.ts index 250f970e87..439ec1b469 100644 --- a/src/services/decision-pack.ts +++ b/src/services/decision-pack.ts @@ -616,7 +616,9 @@ function buildContributorDecisionPack(args: { ? new Map([...args.issueQualityByRepo.entries()].map(([repoFullName, report]) => [repoFullName.toLowerCase(), report])) : new Map(); const languageSet = new Set((args.profile.github?.topLanguages ?? []).map((language) => language.toLowerCase())); - const labelHistory = new Set(args.profile.registeredRepoActivity?.dominantLabels ?? []); + // Case-insensitive, mirroring the opportunity engine (signals/engine.ts) and this file's own languageSet: + // GitHub labels compare case-insensitively, so a mixed-case dominant label must still overlap a config label. + const labelHistory = new Set((args.profile.registeredRepoActivity?.dominantLabels ?? []).map((label) => label.toLowerCase())); const roleContexts = registeredRepositories.map((repo) => buildRoleContext({ login: args.login, @@ -773,7 +775,7 @@ function buildRepoDecision(args: { }; const labelHistory = args.labelHistory; const labelFit = labelHistory - ? Object.keys(args.repo.registryConfig?.labelMultipliers ?? {}).filter((label) => labelHistory.has(label)) + ? Object.keys(args.repo.registryConfig?.labelMultipliers ?? {}).filter((label) => labelHistory.has(label.toLowerCase())) : []; const copyContext: RepoCopyContext = { repoFullName: args.repo.fullName, diff --git a/test/unit/decision-pack.test.ts b/test/unit/decision-pack.test.ts index f57f212c03..9939d654e0 100644 --- a/test/unit/decision-pack.test.ts +++ b/test/unit/decision-pack.test.ts @@ -1379,6 +1379,43 @@ describe("decision-pack service", () => { ).not.toMatch(FORBIDDEN_PUBLIC_TRADEOFF_LANGUAGE); }); + it("matches label history against config labels case-insensitively (regression)", () => { + // A mixed-case dominant label must overlap a differently-cased config label, mirroring the opportunity + // engine and this file's own case-insensitive languageSet. Covers both the built Set and the lookup. + const profile = { + login: "jsonbored", + github: { topLanguages: [] }, + source: {}, + gittensor: null, + registeredRepoActivity: { reposTouched: ["owner/gfi"], dominantLabels: ["Good First Issue"] }, + trustSignals: {}, + } as any; + const pack = __decisionPackInternals.buildContributorDecisionPack({ + login: "jsonbored", + profile, + outcomeHistory: { login: "jsonbored", totals: {}, repoOutcomes: [], successPatterns: [], failurePatterns: [], summary: "" } as any, + repositories: [repoWithLabels("owner/gfi", 0.04, 0, { "good first issue": 1.5 })], + syncStates: [], + syncSegments: [], + totals: [], + scoringModelSnapshotId: "scoring-1", + contributorPullRequests: [], + contributorIssues: [], + openPrMonitor: emptyOpenPrMonitor("jsonbored"), + }); + const decision = pack.repoDecisions.find((d) => d.repoFullName === "owner/gfi")!; + expect(decision.labelFit).toEqual(["good first issue"]); + + // Direct lookup-side check: a mixed-case config key overlaps a lowercased history entry. + const repoDecision = __decisionPackInternals.buildRepoDecision({ + repo: repoWithLabels("owner/lang", 0.005, 0, { "Good First Issue": 1.5 }), + roleContext: { maintainerLane: false } as any, + outcome: { openPullRequests: 0, mergedPullRequests: 1, closedPullRequestRate: 0, credibility: 1 } as any, + labelHistory: new Set(["good first issue"]), + } as any); + expect(repoDecision.labelFit).toEqual(["Good First Issue"]); + }); + it("covers languageMatch true/false and labelFit empty/non-empty paths", () => { const ctx = (overrides: Record = {}) => __decisionPackInternals.buildRepoDecision({