@@ -6,7 +6,20 @@ const { CONFIG } = require('../config');
66const { repoLabelFor } = require ( '../helpers/utils' ) ;
77const { countClosedIssuesByAssignee } = require ( '../api/github-api' ) ;
88
9- // Fast path: user has already completed ≥1 issue at this level or higher
9+ /**
10+ * Fast-path eligibility check.
11+ *
12+ * A contributor is considered eligible for `candidate` if they have already
13+ * completed at least one issue at that level OR any higher level.
14+ *
15+ * This avoids forcing strict progression when the user already has experience.
16+ *
17+ * @param {import('@actions/github').GitHub } github
18+ * @param {object } homeRepo
19+ * @param {string } username
20+ * @param {string } candidate - Canonical level key
21+ * @returns {Promise<boolean> }
22+ */
1023async function passesBypassCheck ( github , homeRepo , username , candidate ) {
1124 const idx = CONFIG . skillHierarchy . indexOf ( candidate ) ;
1225 if ( idx === - 1 ) return false ;
@@ -23,10 +36,16 @@ async function passesBypassCheck(github, homeRepo, username, candidate) {
2336}
2437
2538/**
26- * Checks whether a contributor meets the prerequisite count for a given level.
39+ * Checks whether a contributor meets the prerequisite count for a level.
40+ *
41+ * This enforces the normal progression rules:
42+ * e.g. "3 beginner issues required before intermediate".
2743 *
28- * Unlike the bypass check, this verifies that the contributor has completed
29- * enough issues at the *previous* level to formally qualify for the candidate.
44+ * @param {import('@actions/github').GitHub } github
45+ * @param {object } homeRepo
46+ * @param {string } username
47+ * @param {{ requiredLevel: string, requiredCount: number } } prereq
48+ * @returns {Promise<boolean|null> } null = API failure
3049 */
3150async function passesNormalCheck ( github , homeRepo , username , prereq ) {
3251 const count = await countClosedIssuesByAssignee (
@@ -116,39 +135,61 @@ function adjustEligibilityForCurrentPR(completedKey, eligibleKey) {
116135}
117136
118137/**
119- * Detects whether the current PR completion crossed the threshold into the next level.
120- *
121- * Checks if the contributor has reached *exactly* the required count for the next
122- * level after this PR. Uses cap = requiredCount + 1 to avoid over-fetching.
138+ * Computes next level metadata for unlock detection.
123139 *
124- * Example: beginner → intermediate requires 3 beginner completions.
125- * If the search returns count === 3, this PR was the one that crossed the line.
140+ * Ensures:
141+ * - current level exists
142+ * - next level exists
143+ * - next level depends on current level
126144 *
127- * @param {import('@actions/github').GitHub } github
128- * @param {object } homeRepo - Home repo entry from CONFIG.repos.
129- * @param {string } username - GitHub login of the contributor.
130- * @param {string } currentLevelKey - Canonical key of the level just completed.
131- * @returns {Promise<string|null> } Canonical key of the unlocked level, or null.
145+ * @param {string } currentLevelKey
146+ * @returns {{ nextKey: string, nextPrereq: object } | null }
132147 */
133- async function detectUnlockedLevel ( github , homeRepo , username , currentLevelKey ) {
134- const hierarchy = CONFIG . skillHierarchy ;
148+ function getNextLevelInfo ( currentLevelKey ) {
149+ const hierarchy = CONFIG . skillHierarchy ;
135150 const currentIndex = hierarchy . indexOf ( currentLevelKey ) ;
136-
137151 if ( currentIndex === - 1 ) return null ;
138152
139- const nextKey = hierarchy [ currentIndex + 1 ] ?? null ;
153+ const nextKey = hierarchy [ currentIndex + 1 ] ;
140154 if ( ! nextKey ) return null ;
141155
142156 const nextPrereq = CONFIG . skillPrerequisites [ nextKey ] ;
143- if ( ! nextPrereq ?. requiredLevel || nextPrereq . requiredLevel !== currentLevelKey ) return null ;
157+ if ( ! nextPrereq || nextPrereq . requiredLevel !== currentLevelKey ) return null ;
158+
159+ return { nextKey, nextPrereq } ;
160+ }
161+
162+ /**
163+ * Detects if the contributor just unlocked the next level.
164+ *
165+ * Trigger condition:
166+ * completed count === requiredCount
167+ *
168+ * Uses a capped query (requiredCount + 1) for efficiency.
169+ *
170+ * @param {import('@actions/github').GitHub } github
171+ * @param {object } homeRepo
172+ * @param {string } username
173+ * @param {string } currentLevelKey
174+ * @returns {Promise<string|null> } unlocked level key
175+ */
176+ async function detectUnlockedLevel ( github , homeRepo , username , currentLevelKey ) {
177+ const next = getNextLevelInfo ( currentLevelKey ) ;
178+ if ( ! next ) return null ;
179+
180+ const { nextKey, nextPrereq } = next ;
144181
145182 const count = await countClosedIssuesByAssignee (
146- github , homeRepo . owner , homeRepo . repo , username ,
147- repoLabelFor ( homeRepo , currentLevelKey ) , nextPrereq . requiredCount + 1 ,
183+ github ,
184+ homeRepo . owner ,
185+ homeRepo . repo ,
186+ username ,
187+ repoLabelFor ( homeRepo , currentLevelKey ) ,
188+ nextPrereq . requiredCount + 1 ,
148189 ) ;
149190
150191 if ( count === null ) return null ;
151- // NOTE: count is derived from first-page results only; safe because requiredCount is small.
192+
152193 return count === nextPrereq . requiredCount ? nextKey : null ;
153194}
154195
0 commit comments