@@ -79,6 +79,7 @@ describe("explainScoreBreakdown", () => {
7979 "contributionBonus" ,
8080 "labelMultiplier" ,
8181 "issueMultiplier" ,
82+ "branchEligibility" ,
8283 "credibilityMultiplier" ,
8384 "reviewPenaltyMultiplier" ,
8485 "openPrMultiplier" ,
@@ -92,7 +93,7 @@ describe("explainScoreBreakdown", () => {
9293 expect ( [ "full" , "reduced" , "neutral" , "blocked" ] ) . toContain ( component . band ) ;
9394 }
9495 expect ( breakdown . highestLeverageLever . component ) . toBeTruthy ( ) ;
95- expect ( breakdown . highestLeverageLever . lever ) . toMatch ( / m e r g e | c l o s e | c r e d i b i l i t y | o p e n P R | l i n k e d i s s u e | d e n s i t y | r e v i e w / i) ;
96+ expect ( breakdown . highestLeverageLever . lever ) . toMatch ( / m e r g e | c l o s e | c r e d i b i l i t y | o p e n P R | l i n k e d i s s u e | d e n s i t y | r e v i e w | b r a n c h / i) ;
9697 expect ( JSON . stringify ( breakdown ) ) . not . toMatch ( FORBIDDEN ) ;
9798 // No open issues → within the allowance → full band on the open-issue gate.
9899 expect ( breakdown . components . find ( ( entry ) => entry . component === "openIssueMultiplier" ) ) . toMatchObject ( { band : "full" } ) ;
@@ -120,6 +121,166 @@ describe("explainScoreBreakdown", () => {
120121 expect ( JSON . stringify ( blocked ) ) . not . toMatch ( FORBIDDEN ) ;
121122 } ) ;
122123
124+ it ( "explains branch eligibility as neutral (not required), full (eligible), blocked (ineligible), and reduced (unknown/missing/stale)" , ( ) => {
125+ const notRequired = explainScoreBreakdown (
126+ buildScorePreview ( {
127+ repo,
128+ snapshot,
129+ input : { repoFullName : repo . fullName , sourceTokenScore : 40 , totalTokenScore : 60 , sourceLines : 80 , linkedIssueMode : "none" } ,
130+ } ) ,
131+ ) ;
132+ expect ( notRequired . components . find ( ( entry ) => entry . component === "branchEligibility" ) ) . toMatchObject ( { band : "neutral" } ) ;
133+
134+ const eligible = explainScoreBreakdown (
135+ buildScorePreview ( {
136+ repo,
137+ snapshot,
138+ input : {
139+ repoFullName : repo . fullName ,
140+ sourceTokenScore : 40 ,
141+ totalTokenScore : 60 ,
142+ sourceLines : 80 ,
143+ linkedIssueMode : "standard" ,
144+ linkedIssueContext : { status : "validated" , source : "official_mirror" , issueNumbers : [ 3 ] , solvedByPullRequests : [ 44 ] } ,
145+ branchEligibility : { status : "eligible" , source : "github_metadata" , checkedAt : "2026-05-30T00:00:00.000Z" } ,
146+ } ,
147+ } ) ,
148+ ) ;
149+ expect ( eligible . components . find ( ( entry ) => entry . component === "branchEligibility" ) ) . toMatchObject ( { band : "full" } ) ;
150+
151+ const ineligible = explainScoreBreakdown (
152+ buildScorePreview ( {
153+ repo,
154+ snapshot,
155+ input : {
156+ repoFullName : repo . fullName ,
157+ sourceTokenScore : 40 ,
158+ totalTokenScore : 60 ,
159+ sourceLines : 80 ,
160+ linkedIssueMode : "standard" ,
161+ linkedIssueContext : { status : "validated" , source : "official_mirror" , issueNumbers : [ 3 ] , solvedByPullRequests : [ 44 ] } ,
162+ branchEligibility : { status : "ineligible" , source : "github_metadata" , reason : "head branch is not eligible" } ,
163+ } ,
164+ } ) ,
165+ ) ;
166+ const ineligibleBranch = ineligible . components . find ( ( entry ) => entry . component === "branchEligibility" ) ;
167+ expect ( ineligibleBranch ) . toMatchObject ( { band : "blocked" , leverageScore : 90 } ) ;
168+ expect ( ineligibleBranch ?. summary ) . toMatch ( / h e a d b r a n c h i s n o t e l i g i b l e / i) ;
169+ expect ( ineligibleBranch ?. lever ) . toMatch ( / e l i g i b l e b r a n c h / i) ;
170+
171+ const ineligibleNoReason = explainScoreBreakdown (
172+ buildScorePreview ( {
173+ repo,
174+ snapshot,
175+ input : {
176+ repoFullName : repo . fullName ,
177+ sourceTokenScore : 40 ,
178+ totalTokenScore : 60 ,
179+ sourceLines : 80 ,
180+ linkedIssueMode : "standard" ,
181+ linkedIssueContext : { status : "raw" , source : "github_cache" , issueNumbers : [ 12 ] } ,
182+ branchEligibility : { status : "ineligible" , source : "github_metadata" } ,
183+ } ,
184+ } ) ,
185+ ) ;
186+ expect ( ineligibleNoReason . components . find ( ( entry ) => entry . component === "branchEligibility" ) ?. summary ) . toMatch (
187+ / c o n f i r m e d i n e l i g i b l e ; s t a n d a r d l i n k e d - i s s u e s c o r i n g i s b l o c k e d / i,
188+ ) ;
189+
190+ const unknownMetadata = explainScoreBreakdown (
191+ buildScorePreview ( {
192+ repo,
193+ snapshot,
194+ input : {
195+ repoFullName : repo . fullName ,
196+ sourceTokenScore : 40 ,
197+ totalTokenScore : 60 ,
198+ sourceLines : 80 ,
199+ linkedIssueMode : "standard" ,
200+ linkedIssueContext : { status : "plausible" , source : "github_cache" , issueNumbers : [ 4 ] } ,
201+ branchEligibility : { status : "unknown" , source : "github_metadata" } ,
202+ } ,
203+ } ) ,
204+ ) ;
205+ expect ( unknownMetadata . components . find ( ( entry ) => entry . component === "branchEligibility" ) ?. summary ) . toMatch ( / u n k n o w n / i) ;
206+
207+ const missing = explainScoreBreakdown (
208+ buildScorePreview ( {
209+ repo,
210+ snapshot,
211+ input : {
212+ repoFullName : repo . fullName ,
213+ sourceTokenScore : 40 ,
214+ totalTokenScore : 60 ,
215+ sourceLines : 80 ,
216+ linkedIssueMode : "standard" ,
217+ linkedIssueContext : { status : "raw" , source : "github_cache" , issueNumbers : [ 12 ] } ,
218+ } ,
219+ } ) ,
220+ ) ;
221+ expect ( missing . components . find ( ( entry ) => entry . component === "branchEligibility" ) ) . toMatchObject ( {
222+ band : "reduced" ,
223+ summary : expect . stringMatching ( / e v i d e n c e i s m i s s i n g / i) ,
224+ } ) ;
225+
226+ const stale = explainScoreBreakdown (
227+ buildScorePreview ( {
228+ repo,
229+ snapshot,
230+ input : {
231+ repoFullName : repo . fullName ,
232+ sourceTokenScore : 40 ,
233+ totalTokenScore : 60 ,
234+ sourceLines : 80 ,
235+ linkedIssueMode : "standard" ,
236+ linkedIssueContext : { status : "plausible" , source : "github_cache" , issueNumbers : [ 4 ] } ,
237+ branchEligibility : { status : "eligible" , source : "local_metadata" , stale : true , checkedAt : "2026-05-01T00:00:00.000Z" } ,
238+ } ,
239+ } ) ,
240+ ) ;
241+ expect ( stale . components . find ( ( entry ) => entry . component === "branchEligibility" ) ?. summary ) . toMatch ( / s t a l e / i) ;
242+
243+ const userSupplied = explainScoreBreakdown (
244+ buildScorePreview ( {
245+ repo,
246+ snapshot,
247+ input : {
248+ repoFullName : repo . fullName ,
249+ sourceTokenScore : 40 ,
250+ totalTokenScore : 60 ,
251+ sourceLines : 80 ,
252+ linkedIssueMode : "standard" ,
253+ linkedIssueContext : { status : "plausible" , source : "github_cache" , issueNumbers : [ 4 ] } ,
254+ branchEligibility : { } ,
255+ } ,
256+ } ) ,
257+ ) ;
258+ expect ( userSupplied . components . find ( ( entry ) => entry . component === "branchEligibility" ) ?. summary ) . toMatch ( / u s e r - s u p p l i e d / i) ;
259+ expect ( JSON . stringify ( ineligible ) ) . not . toMatch ( FORBIDDEN ) ;
260+ } ) ;
261+
262+ it ( "prioritizes ineligible branch metadata above invalid linked-issue context" , ( ) => {
263+ const preview = buildScorePreview ( {
264+ repo,
265+ snapshot,
266+ input : {
267+ repoFullName : repo . fullName ,
268+ sourceTokenScore : 80 ,
269+ totalTokenScore : 120 ,
270+ sourceLines : 60 ,
271+ openPrCount : 0 ,
272+ credibility : 1 ,
273+ linkedIssueMode : "standard" ,
274+ linkedIssueContext : { status : "invalid" , source : "github_cache" , issueNumbers : [ 9 ] , reason : "Issue #9 is closed." } ,
275+ branchEligibility : { status : "ineligible" , source : "github_metadata" , reason : "head branch is not eligible" } ,
276+ } ,
277+ } ) ;
278+
279+ const breakdown = explainScoreBreakdown ( preview ) ;
280+ expect ( breakdown . components . find ( ( entry ) => entry . component === "branchEligibility" ) ) . toMatchObject ( { band : "blocked" } ) ;
281+ expect ( breakdown . highestLeverageLever . component ) . toBe ( "branchEligibility" ) ;
282+ } ) ;
283+
123284 it ( "explains an over-threshold open-issue count as a blocked open-issue spam gate" , ( ) => {
124285 const preview = buildScorePreview ( {
125286 repo,
@@ -254,6 +415,7 @@ describe("explainScoreBreakdown", () => {
254415 credibility : 1 ,
255416 linkedIssueMode : "standard" ,
256417 linkedIssueContext : { status : "invalid" , source : "github_cache" , issueNumbers : [ 9 ] , reason : "Issue #9 is closed." } ,
418+ branchEligibility : { status : "eligible" , source : "github_metadata" } ,
257419 } ,
258420 } ) ;
259421
0 commit comments