From 6bdca99eb1b17f565af1bb8b28f93035232f6e4c Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Sun, 5 Jul 2026 05:14:28 +0200 Subject: [PATCH] feat(engine): add pickTopMetadataOpportunities helper Rank metadata discovery candidates and return the top N entries using rankMetadataOpportunities ordering. Lives in metadata-top-pick.ts so vitest patch coverage instruments every branch. Co-authored-by: Cursor --- packages/gittensory-engine/src/index.ts | 1 + .../src/metadata-top-pick.ts | 21 +++++++++ .../unit/opportunity-metadata-signals.test.ts | 45 +++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 packages/gittensory-engine/src/metadata-top-pick.ts diff --git a/packages/gittensory-engine/src/index.ts b/packages/gittensory-engine/src/index.ts index 0bf70e93ea..6535128fff 100644 --- a/packages/gittensory-engine/src/index.ts +++ b/packages/gittensory-engine/src/index.ts @@ -163,3 +163,4 @@ export { type MetadataCandidateIssue, type MetadataRankContext, } from "./opportunity-metadata.js"; +export { pickTopMetadataOpportunities } from "./metadata-top-pick.js"; diff --git a/packages/gittensory-engine/src/metadata-top-pick.ts b/packages/gittensory-engine/src/metadata-top-pick.ts new file mode 100644 index 0000000000..d3f747b32f --- /dev/null +++ b/packages/gittensory-engine/src/metadata-top-pick.ts @@ -0,0 +1,21 @@ +import { + rankMetadataOpportunities, + type MetadataCandidateIssue, + type MetadataRankContext, +} from "./opportunity-metadata.js"; +import type { OpportunityRankInput } from "./opportunity-ranker.js"; + +/** + * Rank metadata candidates and return the top `limit` entries. Non-finite or negative limits return an empty list. + * Pure — delegates to {@link rankMetadataOpportunities} for target filtering, scoring, and tie-breaking. + */ +export function pickTopMetadataOpportunities( + candidates: readonly T[], + context: MetadataRankContext, + limit: number, +): Array { + if (!Number.isFinite(limit)) return []; + const safeLimit = Math.max(0, Math.trunc(limit)); + if (safeLimit === 0 || candidates.length === 0) return []; + return rankMetadataOpportunities(candidates, context).slice(0, safeLimit); +} diff --git a/test/unit/opportunity-metadata-signals.test.ts b/test/unit/opportunity-metadata-signals.test.ts index 60488d9277..b2d5dafa5b 100644 --- a/test/unit/opportunity-metadata-signals.test.ts +++ b/test/unit/opportunity-metadata-signals.test.ts @@ -7,6 +7,7 @@ import { opportunityMetadataInternals, rankMetadataOpportunities, } from "../../packages/gittensory-engine/src/opportunity-metadata"; +import { pickTopMetadataOpportunities } from "../../packages/gittensory-engine/src/metadata-top-pick"; import { DEFAULT_MINER_GOAL_SPEC } from "../../packages/gittensory-engine/src/miner-goal-spec"; import { computeOpportunityCompetition } from "../../packages/gittensory-engine/src/opportunity-competition"; import { computeOpportunityFreshness } from "../../packages/gittensory-engine/src/opportunity-freshness"; @@ -120,6 +121,50 @@ describe("opportunity metadata signals", () => { expect(ranked.map((entry) => entry.issueNumber)).toEqual([1, 2]); }); + it("pickTopMetadataOpportunities returns the highest-scoring metadata candidates up to the limit", () => { + const candidates = [ + { ...base, issueNumber: 1, labels: ["wontfix"] }, + { ...base, issueNumber: 2, labels: ["help wanted"] }, + { ...base, issueNumber: 3, labels: ["help wanted", "bug"] }, + ]; + const topTwo = pickTopMetadataOpportunities(candidates, { nowMs: NOW }, 2); + expect(topTwo.map((entry) => entry.issueNumber)).toEqual([3, 2]); + expect(topTwo[0]!.rankScore).toBeGreaterThan(topTwo[1]!.rankScore); + }); + + it("pickTopMetadataOpportunities skips miner-disabled repos before slicing", () => { + const candidates = [ + { ...base, issueNumber: 1, repoFullName: "acme/disabled" }, + { ...base, issueNumber: 2, labels: ["help wanted"] }, + ]; + const ranked = pickTopMetadataOpportunities(candidates, { + nowMs: NOW, + goalSpecsByRepo: { + "acme/disabled": { ...DEFAULT_MINER_GOAL_SPEC, minerEnabled: false }, + }, + }, 5); + expect(ranked.map((entry) => entry.issueNumber)).toEqual([2]); + }); + + it("pickTopMetadataOpportunities returns an empty list for invalid limits or no candidates", () => { + const candidates = [{ ...base, issueNumber: 1 }]; + expect(pickTopMetadataOpportunities(candidates, { nowMs: NOW }, 0)).toEqual([]); + expect(pickTopMetadataOpportunities(candidates, { nowMs: NOW }, -1)).toEqual([]); + expect(pickTopMetadataOpportunities(candidates, { nowMs: NOW }, Number.NaN)).toEqual([]); + expect(pickTopMetadataOpportunities([], { nowMs: NOW }, 3)).toEqual([]); + }); + + it("pickTopMetadataOpportunities is exported from the package barrel", async () => { + const barrel = await import("../../packages/gittensory-engine/src/index"); + expect(typeof barrel.pickTopMetadataOpportunities).toBe("function"); + const top = barrel.pickTopMetadataOpportunities( + [{ ...base, issueNumber: 9, labels: ["help wanted"] }], + { nowMs: NOW }, + 1, + ); + expect(top.map((entry) => entry.issueNumber)).toEqual([9]); + }); + it("freshness and competition helpers stay pure with injected clocks and safe inputs", () => { expect(computeOpportunityFreshness([{ state: "closed", updatedAt: "2026-07-03T00:00:00.000Z" }], NOW)).toBe(0); expect(computeOpportunityCompetition(Number.NaN, 3)).toBe(1);