Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/loopover-engine/src/miner-goal-lane-fit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] },
Expand All @@ -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) {
Expand Down
6 changes: 3 additions & 3 deletions packages/loopover-engine/test/miner-goal-lane-fit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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", () => {
Expand Down
6 changes: 3 additions & 3 deletions test/unit/metadata-min-score.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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]);
Expand Down
6 changes: 3 additions & 3 deletions test/unit/metadata-top-min-score.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
4 changes: 2 additions & 2 deletions test/unit/miner-goal-lane-fit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});