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
1 change: 1 addition & 0 deletions packages/gittensory-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export {
rankOpportunities,
type OpportunityRankInput,
} from "./opportunity-ranker.js";
export { rankOpportunitiesAtOrAboveScore } from "./ranked-opportunity-min-score.js";
export {
extractObjectiveAnchorHistory,
extractObjectiveAnchorFeatures,
Expand Down
15 changes: 15 additions & 0 deletions packages/gittensory-engine/src/ranked-opportunity-min-score.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { rankOpportunities, type OpportunityRankInput } from "./opportunity-ranker.js";

/**
* Rank candidates and keep only those whose {@link rankOpportunityScore} is at or above `minScore`.
* Non-finite thresholds return an empty list. Pure — delegates ordering to {@link rankOpportunities}.
*/
export function rankOpportunitiesAtOrAboveScore<T>(
candidates: Array<T & OpportunityRankInput>,
minScore: number,
): Array<Omit<T, "rankScore"> & OpportunityRankInput & { rankScore: number }> {
if (!Number.isFinite(minScore)) return [];
if (candidates.length === 0) return [];
const threshold = Math.min(1, Math.max(0, minScore));
return rankOpportunities(candidates).filter((entry) => entry.rankScore >= threshold);
}
18 changes: 17 additions & 1 deletion packages/gittensory-engine/test/opportunity-ranker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
// (dist/index.js) so the export contract itself is exercised. Pure module — no network, never flakes.
import { test } from "node:test";
import assert from "node:assert/strict";
import { rankOpportunityScore, rankOpportunities, pickTopRankedOpportunities } from "../dist/index.js";
import { rankOpportunityScore, rankOpportunities, pickTopRankedOpportunities, rankOpportunitiesAtOrAboveScore } from "../dist/index.js";

test("barrel: the public entrypoint re-exports the ranker API", () => {
assert.equal(typeof rankOpportunityScore, "function");
assert.equal(typeof rankOpportunities, "function");
assert.equal(typeof pickTopRankedOpportunities, "function");
assert.equal(typeof rankOpportunitiesAtOrAboveScore, "function");
});

const full = { potential: 1, feasibility: 1, laneFit: 1, freshness: 1, dupRisk: 0 };
Expand Down Expand Up @@ -120,3 +121,18 @@ test("pickTopRankedOpportunities: rejects non-finite limits", () => {
assert.deepEqual(pickTopRankedOpportunities(candidates, Number.NaN), []);
assert.deepEqual(pickTopRankedOpportunities(candidates, Number.POSITIVE_INFINITY), []);
});

test("rankOpportunitiesAtOrAboveScore: keeps ranked candidates at or above the threshold", () => {
const candidates = [
{ id: "low", potential: 0.2, feasibility: 1, laneFit: 1, freshness: 1, dupRisk: 0 },
{ id: "high", potential: 0.9, feasibility: 1, laneFit: 1, freshness: 1, dupRisk: 0 },
{ id: "mid", potential: 0.5, feasibility: 1, laneFit: 1, freshness: 1, dupRisk: 0 },
];
const filtered = rankOpportunitiesAtOrAboveScore(candidates, 0.5);
assert.deepEqual(filtered.map((entry) => entry.id), ["high", "mid"]);
});

test("rankOpportunitiesAtOrAboveScore: rejects non-finite thresholds", () => {
const candidates = [{ id: "only", ...full }];
assert.deepEqual(rankOpportunitiesAtOrAboveScore(candidates, Number.NaN), []);
});
50 changes: 50 additions & 0 deletions test/unit/opportunity-ranker.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import { pickTopRankedOpportunities, rankOpportunities, rankOpportunityScore, type OpportunityRankInput } from "../../packages/gittensory-engine/src/opportunity-ranker";
import { rankOpportunitiesAtOrAboveScore } from "../../packages/gittensory-engine/src/ranked-opportunity-min-score";

// A neutral, all-passing candidate (every factor 1, no contention → score 1); tests override one field at a time.
function input(over: Partial<OpportunityRankInput> = {}): OpportunityRankInput {
Expand Down Expand Up @@ -186,3 +187,52 @@ describe("pickTopRankedOpportunities", () => {
expect(barrel.pickTopRankedOpportunities(candidates, 1).map((entry) => entry.id)).toEqual(["top"]);
});
});

describe("rankOpportunitiesAtOrAboveScore", () => {
const candidates = [
{ id: "mid", ...input({ potential: 0.5 }) },
{ id: "top", ...input() },
{ id: "low", ...input({ freshness: 0.25 }) },
];

it("keeps only candidates at or above the score threshold in rank order", () => {
const filtered = rankOpportunitiesAtOrAboveScore(candidates, 0.5);
expect(filtered.map((entry) => entry.id)).toEqual(["top", "mid"]);
expect(filtered.every((entry) => entry.rankScore >= 0.5)).toBe(true);
});

it("returns every candidate when the threshold is zero", () => {
expect(rankOpportunitiesAtOrAboveScore(candidates, 0).map((entry) => entry.id)).toEqual([
"top",
"mid",
"low",
]);
});

it("returns only perfect scores when the threshold is one", () => {
expect(rankOpportunitiesAtOrAboveScore(candidates, 1).map((entry) => entry.id)).toEqual(["top"]);
});

it("returns an empty array for a non-finite threshold or no candidates", () => {
expect(rankOpportunitiesAtOrAboveScore(candidates, Number.NaN)).toEqual([]);
expect(rankOpportunitiesAtOrAboveScore(candidates, Number.POSITIVE_INFINITY)).toEqual([]);
expect(rankOpportunitiesAtOrAboveScore([], 0.5)).toEqual([]);
});

it("clamps out-of-range thresholds before filtering", () => {
expect(rankOpportunitiesAtOrAboveScore(candidates, -0.5).map((entry) => entry.id)).toEqual([
"top",
"mid",
"low",
]);
expect(rankOpportunitiesAtOrAboveScore(candidates, 1.5).map((entry) => entry.id)).toEqual(["top"]);
});

it("is exported from the package barrel", async () => {
const barrel = await import("../../packages/gittensory-engine/src/index");
expect(typeof barrel.rankOpportunitiesAtOrAboveScore).toBe("function");
expect(
barrel.rankOpportunitiesAtOrAboveScore(candidates, 0.5).map((entry: { id: string }) => entry.id),
).toEqual(["top", "mid"]);
});
});
Loading