diff --git a/packages/loopover-engine/src/miner-goal-lane-fit.ts b/packages/loopover-engine/src/miner-goal-lane-fit.ts index 8ef0ebff50..b8bc3645a0 100644 --- a/packages/loopover-engine/src/miner-goal-lane-fit.ts +++ b/packages/loopover-engine/src/miner-goal-lane-fit.ts @@ -21,6 +21,10 @@ function normalizeLabels(labels: readonly string[]): string[] { /** * Compute a [0, 1] lane-fit score from issue labels and a parsed {@link MinerGoalSpec}. Path-based fit is * intentionally omitted — discovery metadata has labels only; path gating belongs in the analyze phase. + * + * The no-preference neutral score matches {@link computeLaneFit}'s rule 2 (`0.5`, unopinionated) rather + * than `1`: an unconfigured `preferredLabels` means the operator has no opinion, not that every issue is + * a perfect fit (#8870). */ export function computeMinerGoalLaneFit( issue: { labels: readonly string[] }, @@ -36,7 +40,7 @@ export function computeMinerGoalLaneFit( let score: number; if (preferred.length === 0) { - score = 1; + score = 0.5; } else { const preferredMatch = preferred.some((want) => issueLabels.includes(want)); if (preferredMatch) { diff --git a/packages/loopover-engine/test/miner-goal-lane-fit.test.ts b/packages/loopover-engine/test/miner-goal-lane-fit.test.ts index 756a602b7a..6ba5d13a27 100644 --- a/packages/loopover-engine/test/miner-goal-lane-fit.test.ts +++ b/packages/loopover-engine/test/miner-goal-lane-fit.test.ts @@ -9,8 +9,8 @@ test("isMinerRepoTargetable respects minerEnabled opt-out", () => { assert.equal(isMinerRepoTargetable({ ...DEFAULT_MINER_GOAL_SPEC, minerEnabled: false }), false); }); -test("computeMinerGoalLaneFit returns 1 when no preferred labels are configured", () => { - assert.equal(computeMinerGoalLaneFit({ labels: ["docs"] }, DEFAULT_MINER_GOAL_SPEC), 1); +test("computeMinerGoalLaneFit returns the neutral 0.5 when no preferred labels are configured", () => { + assert.equal(computeMinerGoalLaneFit({ labels: ["docs"] }, DEFAULT_MINER_GOAL_SPEC), 0.5); }); test("computeMinerGoalLaneFit matches preferred labels case-insensitively", () => { @@ -39,7 +39,7 @@ test("computeMinerGoalLaneFit applies issueDiscoveryPolicy modifiers", () => { test("computeMinerGoalLaneFit returns 0 when a blocked label matches case-insensitively", () => { const spec = { ...DEFAULT_MINER_GOAL_SPEC, blockedLabels: ["wontfix"] }; assert.equal(computeMinerGoalLaneFit({ labels: ["WontFix"] }, spec), 0); - assert.equal(computeMinerGoalLaneFit({ labels: ["bug"] }, spec), 1); + assert.equal(computeMinerGoalLaneFit({ labels: ["bug"] }, spec), 0.5); }); test("computeMinerGoalLaneFit ignores malformed label entries safely", () => { diff --git a/test/unit/metadata-min-score.test.ts b/test/unit/metadata-min-score.test.ts index 1311b12062..c5095fc0fd 100644 --- a/test/unit/metadata-min-score.test.ts +++ b/test/unit/metadata-min-score.test.ts @@ -23,9 +23,9 @@ describe("rankMetadataOpportunitiesAtOrAboveScore", () => { ]; it("keeps only metadata candidates at or above the score threshold in rank order", () => { - const filtered = rankMetadataOpportunitiesAtOrAboveScore(candidates, { nowMs: NOW }, 0.1); + const filtered = rankMetadataOpportunitiesAtOrAboveScore(candidates, { nowMs: NOW }, 0.05); expect(filtered.map((entry) => entry.issueNumber)).toEqual([3, 2]); - expect(filtered.every((entry) => entry.rankScore >= 0.1)).toBe(true); + expect(filtered.every((entry) => entry.rankScore >= 0.05)).toBe(true); }); it("returns every targetable candidate when the threshold is zero", () => { @@ -72,7 +72,7 @@ describe("rankMetadataOpportunitiesAtOrAboveScore", () => { const barrel = await import("../../packages/loopover-engine/src/index"); expect(typeof barrel.rankMetadataOpportunitiesAtOrAboveScore).toBe("function"); expect( - barrel.rankMetadataOpportunitiesAtOrAboveScore(candidates, { nowMs: NOW }, 0.1).map( + barrel.rankMetadataOpportunitiesAtOrAboveScore(candidates, { nowMs: NOW }, 0.05).map( (entry: { issueNumber: number }) => entry.issueNumber, ), ).toEqual([3, 2]); diff --git a/test/unit/metadata-top-min-score.test.ts b/test/unit/metadata-top-min-score.test.ts index 2d81a9543f..b7a11dbf25 100644 --- a/test/unit/metadata-top-min-score.test.ts +++ b/test/unit/metadata-top-min-score.test.ts @@ -24,14 +24,14 @@ describe("pickTopMetadataOpportunitiesAtOrAboveScore", () => { ]; it("returns the top survivors after applying the score threshold", () => { - const topTwo = pickTopMetadataOpportunitiesAtOrAboveScore(candidates, { nowMs: NOW }, 0.1, 2); + const topTwo = pickTopMetadataOpportunitiesAtOrAboveScore(candidates, { nowMs: NOW }, 0.05, 2); expect(topTwo.map((entry) => entry.issueNumber)).toEqual([3, 2]); - expect(topTwo.every((entry) => entry.rankScore >= 0.1)).toBe(true); + expect(topTwo.every((entry) => entry.rankScore >= 0.05)).toBe(true); }); it("returns every qualifying candidate when the limit exceeds the filtered list", () => { expect( - pickTopMetadataOpportunitiesAtOrAboveScore(candidates, { nowMs: NOW }, 0.1, 10).map( + pickTopMetadataOpportunitiesAtOrAboveScore(candidates, { nowMs: NOW }, 0.05, 10).map( (entry) => entry.issueNumber, ), ).toEqual([3, 2, 4]); diff --git a/test/unit/miner-goal-lane-fit.test.ts b/test/unit/miner-goal-lane-fit.test.ts index 4f67e8d8e6..68a49867df 100644 --- a/test/unit/miner-goal-lane-fit.test.ts +++ b/test/unit/miner-goal-lane-fit.test.ts @@ -89,7 +89,7 @@ describe("computeMinerGoalLaneFit", () => { expect(computeMinerGoalLaneFit({ labels: ["feature"] }, spec)).toBe(0.25); }); - it("scores normally when no blocked labels are configured", () => { - expect(computeMinerGoalLaneFit({ labels: ["docs"] }, DEFAULT_MINER_GOAL_SPEC)).toBe(1); + it("returns the neutral 0.5 when no preferred labels are configured", () => { + expect(computeMinerGoalLaneFit({ labels: ["docs"] }, DEFAULT_MINER_GOAL_SPEC)).toBe(0.5); }); });