Skip to content
Closed
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
57 changes: 57 additions & 0 deletions src/services/score-breakdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,62 @@ function issueMultiplierBreakdown(preview: ScorePreviewResult): ScoreMultiplierB
return { component: "issueMultiplier", band, summary, lever, leverageScore };
}

// Sibling of issueMultiplierBreakdown: branch/base eligibility gates the standard linked-issue multiplier
// even when issue metadata looks plausible (#90 / #178). Surfaced here so miners see the same actionable
// breakdown other eligibility gates already provide.
function branchEligibilityBreakdown(preview: ScorePreviewResult): ScoreMultiplierBreakdown {
const branch = preview.branchEligibility;
if (!branch.required || branch.status === "not_required") {
return {
component: "branchEligibility",
band: "neutral",
summary: "Branch eligibility is not required for this preview (no standard linked-issue lane).",
lever: "Use standard linked-issue mode when branch/base proof is required for issue-solving PRs.",
leverageScore: 0,
};
}
if (
branch.status === "eligible" &&
branch.evidence === "provided" &&
!branch.stale &&
branch.source !== "user_supplied"
) {
return {
component: "branchEligibility",
band: "full",
summary: "Branch/base eligibility is confirmed for standard linked-issue scoring.",
lever: "Keep branch and base metadata aligned with the repo's registered eligibility rules.",
leverageScore: 5,
};
}
if (branch.status === "ineligible") {
return {
component: "branchEligibility",
band: "blocked",
summary: branch.reason
? `Branch/base eligibility is confirmed ineligible (${branch.reason}).`
: "Branch/base eligibility is confirmed ineligible; standard linked-issue scoring is blocked.",
lever: "Use an eligible branch or remove linked-issue assumptions before relying on this preview.",
leverageScore: 90,
};
}
const summary =
branch.evidence === "missing"
? "Branch eligibility evidence is missing; standard linked-issue multiplier assumptions are not confirmed."
: branch.stale
? "Branch eligibility evidence is stale; standard linked-issue multiplier assumptions need refresh."
: branch.status === "unknown" && branch.source === "user_supplied"
? "Branch eligibility evidence is user-supplied; verified metadata is required for standard linked-issue scoring."
: branch.status === "unknown"
? "Branch eligibility is unknown; standard linked-issue multiplier assumptions are not confirmed."
: branch.source === "user_supplied"
? "Branch eligibility evidence is user-supplied; verified metadata is required for standard linked-issue scoring."
: "Branch eligibility is not confirmed; standard linked-issue multiplier assumptions are not applied.";
const lever = "Refresh branch/base eligibility metadata before relying on linked-issue assumptions.";
const leverageScore = branch.evidence === "missing" || branch.status === "unknown" ? 75 : 65;
return { component: "branchEligibility", band: "reduced", summary, lever, leverageScore };
}

function reviewPenaltyBreakdown(preview: ScorePreviewResult): ScoreMultiplierBreakdown {
const { reviewPenaltyMultiplier } = preview.scoreEstimate;
const band = bandForMultiplier(reviewPenaltyMultiplier, false);
Expand Down Expand Up @@ -312,6 +368,7 @@ export function explainScoreBreakdown(preview: ScorePreviewResult): ScoreBreakdo
contributionBonusBreakdown(preview),
labelMultiplierBreakdown(preview),
issueMultiplierBreakdown(preview),
branchEligibilityBreakdown(preview),
credibilityBreakdown(preview),
reviewPenaltyBreakdown(preview),
reviewCollateralBreakdown(preview),
Expand Down
184 changes: 183 additions & 1 deletion test/unit/score-breakdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ describe("explainScoreBreakdown", () => {
"contributionBonus",
"labelMultiplier",
"issueMultiplier",
"branchEligibility",
"credibilityMultiplier",
"reviewPenaltyMultiplier",
"reviewCollateralMultiplier",
Expand All @@ -94,7 +95,7 @@ describe("explainScoreBreakdown", () => {
expect(["full", "reduced", "neutral", "blocked"]).toContain(component.band);
}
expect(breakdown.highestLeverageLever.component).toBeTruthy();
expect(breakdown.highestLeverageLever.lever).toMatch(/merge|close|credibility|open PR|linked issue|density|review/i);
expect(breakdown.highestLeverageLever.lever).toMatch(/merge|close|credibility|open PR|linked issue|density|review|branch/i);
expect(JSON.stringify(breakdown)).not.toMatch(FORBIDDEN);
// No open issues → within the allowance → full band on the open-issue gate.
expect(breakdown.components.find((entry) => entry.component === "openIssueMultiplier")).toMatchObject({ band: "full" });
Expand Down Expand Up @@ -122,6 +123,186 @@ describe("explainScoreBreakdown", () => {
expect(JSON.stringify(blocked)).not.toMatch(FORBIDDEN);
});

it("explains branch eligibility as neutral (not required), full (eligible), blocked (ineligible), and reduced (unknown/missing/stale)", () => {
const notRequired = explainScoreBreakdown(
buildScorePreview({
repo,
snapshot,
input: { repoFullName: repo.fullName, sourceTokenScore: 40, totalTokenScore: 60, sourceLines: 80, linkedIssueMode: "none" },
}),
);
expect(notRequired.components.find((entry) => entry.component === "branchEligibility")).toMatchObject({ band: "neutral" });

const eligible = explainScoreBreakdown(
buildScorePreview({
repo,
snapshot,
input: {
repoFullName: repo.fullName,
sourceTokenScore: 40,
totalTokenScore: 60,
sourceLines: 80,
linkedIssueMode: "standard",
linkedIssueContext: { status: "validated", source: "official_mirror", issueNumbers: [3], solvedByPullRequests: [44] },
branchEligibility: { status: "eligible", source: "github_metadata", checkedAt: "2026-05-30T00:00:00.000Z" },
},
}),
);
expect(eligible.components.find((entry) => entry.component === "branchEligibility")).toMatchObject({ band: "full" });

const userSuppliedEligible = explainScoreBreakdown(
buildScorePreview({
repo,
snapshot,
input: {
repoFullName: repo.fullName,
sourceTokenScore: 40,
totalTokenScore: 60,
sourceLines: 80,
linkedIssueMode: "standard",
linkedIssueContext: { status: "validated", source: "official_mirror", issueNumbers: [3], solvedByPullRequests: [44] },
branchEligibility: { status: "eligible", source: "user_supplied" },
},
}),
);
expect(userSuppliedEligible.components.find((entry) => entry.component === "branchEligibility")).toMatchObject({
band: "reduced",
summary: expect.stringMatching(/user-supplied/i),
});

const ineligible = explainScoreBreakdown(
buildScorePreview({
repo,
snapshot,
input: {
repoFullName: repo.fullName,
sourceTokenScore: 40,
totalTokenScore: 60,
sourceLines: 80,
linkedIssueMode: "standard",
linkedIssueContext: { status: "validated", source: "official_mirror", issueNumbers: [3], solvedByPullRequests: [44] },
branchEligibility: { status: "ineligible", source: "github_metadata", reason: "head branch is not eligible" },
},
}),
);
const ineligibleBranch = ineligible.components.find((entry) => entry.component === "branchEligibility");
expect(ineligibleBranch).toMatchObject({ band: "blocked", leverageScore: 90 });
expect(ineligibleBranch?.summary).toMatch(/head branch is not eligible/i);
expect(ineligibleBranch?.lever).toMatch(/eligible branch/i);

const ineligibleNoReason = explainScoreBreakdown(
buildScorePreview({
repo,
snapshot,
input: {
repoFullName: repo.fullName,
sourceTokenScore: 40,
totalTokenScore: 60,
sourceLines: 80,
linkedIssueMode: "standard",
linkedIssueContext: { status: "raw", source: "github_cache", issueNumbers: [12] },
branchEligibility: { status: "ineligible", source: "github_metadata" },
},
}),
);
expect(ineligibleNoReason.components.find((entry) => entry.component === "branchEligibility")?.summary).toMatch(
/confirmed ineligible; standard linked-issue scoring is blocked/i,
);

const unknownMetadata = explainScoreBreakdown(
buildScorePreview({
repo,
snapshot,
input: {
repoFullName: repo.fullName,
sourceTokenScore: 40,
totalTokenScore: 60,
sourceLines: 80,
linkedIssueMode: "standard",
linkedIssueContext: { status: "plausible", source: "github_cache", issueNumbers: [4] },
branchEligibility: { status: "unknown", source: "github_metadata" },
},
}),
);
expect(unknownMetadata.components.find((entry) => entry.component === "branchEligibility")?.summary).toMatch(/unknown/i);

const missing = explainScoreBreakdown(
buildScorePreview({
repo,
snapshot,
input: {
repoFullName: repo.fullName,
sourceTokenScore: 40,
totalTokenScore: 60,
sourceLines: 80,
linkedIssueMode: "standard",
linkedIssueContext: { status: "raw", source: "github_cache", issueNumbers: [12] },
},
}),
);
expect(missing.components.find((entry) => entry.component === "branchEligibility")).toMatchObject({
band: "reduced",
summary: expect.stringMatching(/evidence is missing/i),
});

const stale = explainScoreBreakdown(
buildScorePreview({
repo,
snapshot,
input: {
repoFullName: repo.fullName,
sourceTokenScore: 40,
totalTokenScore: 60,
sourceLines: 80,
linkedIssueMode: "standard",
linkedIssueContext: { status: "plausible", source: "github_cache", issueNumbers: [4] },
branchEligibility: { status: "eligible", source: "local_metadata", stale: true, checkedAt: "2026-05-01T00:00:00.000Z" },
},
}),
);
expect(stale.components.find((entry) => entry.component === "branchEligibility")?.summary).toMatch(/stale/i);

const userSupplied = explainScoreBreakdown(
buildScorePreview({
repo,
snapshot,
input: {
repoFullName: repo.fullName,
sourceTokenScore: 40,
totalTokenScore: 60,
sourceLines: 80,
linkedIssueMode: "standard",
linkedIssueContext: { status: "plausible", source: "github_cache", issueNumbers: [4] },
branchEligibility: { status: "unknown" },
},
}),
);
expect(userSupplied.components.find((entry) => entry.component === "branchEligibility")?.summary).toMatch(/user-supplied/i);
expect(JSON.stringify(ineligible)).not.toMatch(FORBIDDEN);
});

it("prioritizes ineligible branch metadata above invalid linked-issue context", () => {
const preview = buildScorePreview({
repo,
snapshot,
input: {
repoFullName: repo.fullName,
sourceTokenScore: 80,
totalTokenScore: 120,
sourceLines: 60,
openPrCount: 0,
credibility: 1,
linkedIssueMode: "standard",
linkedIssueContext: { status: "invalid", source: "github_cache", issueNumbers: [9], reason: "Issue #9 is closed." },
branchEligibility: { status: "ineligible", source: "github_metadata", reason: "head branch is not eligible" },
},
});

const breakdown = explainScoreBreakdown(preview);
expect(breakdown.components.find((entry) => entry.component === "branchEligibility")).toMatchObject({ band: "blocked" });
expect(breakdown.highestLeverageLever.component).toBe("branchEligibility");
});

it("explains an over-threshold open-issue count as a blocked open-issue spam gate", () => {
const preview = buildScorePreview({
repo,
Expand Down Expand Up @@ -374,6 +555,7 @@ describe("explainScoreBreakdown", () => {
credibility: 1,
linkedIssueMode: "standard",
linkedIssueContext: { status: "invalid", source: "github_cache", issueNumbers: [9], reason: "Issue #9 is closed." },
branchEligibility: { status: "eligible", source: "github_metadata" },
},
});

Expand Down
Loading