diff --git a/packages/gittensory-engine/src/index.ts b/packages/gittensory-engine/src/index.ts index 2d14432701..a188c66259 100644 --- a/packages/gittensory-engine/src/index.ts +++ b/packages/gittensory-engine/src/index.ts @@ -12,6 +12,7 @@ export { } from "./opportunity-ranker.js"; export { rankOpportunitiesAtOrAboveScore } from "./ranked-opportunity-min-score.js"; export { pickTopRankedOpportunitiesAtOrAboveScore } from "./ranked-opportunity-top-min-score.js"; +export { bestRankedOpportunity } from "./ranked-opportunity-best-pick.js"; export { extractObjectiveAnchorHistory, extractObjectiveAnchorFeatures, diff --git a/packages/gittensory-engine/src/ranked-opportunity-best-pick.ts b/packages/gittensory-engine/src/ranked-opportunity-best-pick.ts new file mode 100644 index 0000000000..c90f947951 --- /dev/null +++ b/packages/gittensory-engine/src/ranked-opportunity-best-pick.ts @@ -0,0 +1,12 @@ +import { rankOpportunities, type OpportunityRankInput } from "./opportunity-ranker.js"; + +/** + * Return the highest-scoring ranked candidate, or `null` when the list is empty. + * Pure — delegates to {@link rankOpportunities} for scoring and tie-breaking. + */ +export function bestRankedOpportunity( + candidates: Array, +): (Omit & OpportunityRankInput & { rankScore: number }) | null { + const ranked = rankOpportunities(candidates); + return ranked[0] ?? null; +} diff --git a/test/unit/ranked-opportunity-best-pick.test.ts b/test/unit/ranked-opportunity-best-pick.test.ts new file mode 100644 index 0000000000..e302b289f7 --- /dev/null +++ b/test/unit/ranked-opportunity-best-pick.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; + +import { bestRankedOpportunity } from "../../packages/gittensory-engine/src/ranked-opportunity-best-pick"; +import type { OpportunityRankInput } from "../../packages/gittensory-engine/src/opportunity-ranker"; + +function input(over: Partial = {}): OpportunityRankInput { + return { potential: 1, feasibility: 1, laneFit: 1, freshness: 1, dupRisk: 0, ...over }; +} + +describe("bestRankedOpportunity", () => { + it("returns null for an empty candidate list", () => { + expect(bestRankedOpportunity([])).toBeNull(); + }); + + it("returns the highest-scoring candidate", () => { + const best = bestRankedOpportunity([ + { id: "low", ...input({ potential: 0.2 }) }, + { id: "mid", ...input({ potential: 0.5 }) }, + { id: "top", ...input() }, + ]); + expect(best?.id).toBe("top"); + expect(best?.rankScore).toBe(1); + }); + + it("breaks score ties by input order", () => { + const tie = input({ potential: 0.5, feasibility: 0.5, laneFit: 0.5, freshness: 0.5 }); + const best = bestRankedOpportunity([ + { id: "first", ...tie }, + { id: "second", ...tie }, + ]); + expect(best?.id).toBe("first"); + }); + + it("is exported from the package barrel", async () => { + const barrel = await import("../../packages/gittensory-engine/src/index"); + expect(typeof barrel.bestRankedOpportunity).toBe("function"); + expect(barrel.bestRankedOpportunity([{ id: "solo", ...input() }])?.id).toBe("solo"); + }); +});