Skip to content

Commit 0d9b322

Browse files
authored
Enhance comments and refactor eligibility logic
Refactor eligibility checks and improve comments for clarity. Signed-off-by: parvninama <ninamaparv@gmail.com>
1 parent 9aae705 commit 0d9b322

1 file changed

Lines changed: 89 additions & 45 deletions

File tree

.github/scripts/shared/core/eligibility.js

Lines changed: 89 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -59,83 +59,104 @@ async function passesNormalCheck(github, homeRepo, username, prereq) {
5959
/**
6060
* Determines whether a contributor is eligible for a single candidate level.
6161
*
62-
* Two checks are tried in order:
62+
* Checks are evaluated in order:
6363
* 1. Floor level (no prerequisite) — always eligible.
64-
* 2. Bypass — has already closed ≥1 issue at this level or higher.
65-
* 3. Normal — has met the prerequisite count at the previous level.
64+
* 2. Bypass — contributor already completed ≥1 issue at this level or higher.
65+
* 3. Normal progression — contributor met prerequisite count requirements.
6666
*
67-
* Returns null on API failure so the caller can conservatively skip the candidate.
67+
* API failures are treated conservatively as ineligible.
6868
*
6969
* @param {import('@actions/github').GitHub} github
70-
* @param {object} homeRepo - Home repo entry from CONFIG.repos.
71-
* @param {string} username - GitHub login of the contributor.
72-
* @param {string} candidate - Canonical level key being evaluated.
73-
* @returns {Promise<boolean>} True if eligible, false otherwise (API failures treated as false).
70+
* @param {object} homeRepo
71+
* @param {string} username
72+
* @param {string} candidate - Canonical level key being evaluated
73+
* @returns {Promise<boolean>}
7474
*/
7575
async function isEligibleFor(github, homeRepo, username, candidate) {
7676
const prereq = CONFIG.skillPrerequisites[candidate];
7777

78-
// Floor level (gfi) has no prerequisite — always eligible as a floor.
78+
// Entry-level floor (gfi) is always eligible.
7979
if (!prereq?.requiredLevel) return true;
8080

81-
const bypass = await passesBypassCheck(github, homeRepo, username, candidate);
81+
const bypass = await passesBypassCheck(
82+
github,
83+
homeRepo,
84+
username,
85+
candidate,
86+
);
87+
8288
if (bypass) return true;
8389

84-
// null (API failure) is treated as false — caller skips conservatively.
85-
const normal = await passesNormalCheck(github, homeRepo, username, prereq);
90+
// API failures are treated conservatively as false.
91+
const normal = await passesNormalCheck(
92+
github,
93+
homeRepo,
94+
username,
95+
prereq,
96+
);
97+
8698
return normal === true;
8799
}
88100

89101
/**
90-
* Determines the highest skill level a contributor is eligible for,
91-
* based solely on their *historical* closed issues (excludes this PR).
102+
* Resolves the highest level the contributor is historically eligible for.
92103
*
93-
* Walks the hierarchy from highest to lowest and returns the first
94-
* level for which the contributor is eligible. The result acts as a
95-
* ceiling — recommendations will not exceed this level.
104+
* Historical eligibility intentionally excludes the current PR being processed.
105+
* The result acts as an eligibility ceiling for recommendations.
96106
*
97-
* The caller is responsible for adjusting this ceiling to account for
98-
* the current PR's completion (see adjustEligibilityForCurrentPR).
107+
* The caller is responsible for projecting the current PR completion
108+
* via adjustEligibilityForCurrentPR().
99109
*
100110
* @param {import('@actions/github').GitHub} github
101-
* @param {object} homeRepo - Home repo entry from CONFIG.repos.
102-
* @param {string} username - GitHub login of the contributor.
103-
* @returns {Promise<string>} Canonical level key of the eligibility ceiling.
111+
* @param {object} homeRepo
112+
* @param {string} username
113+
* @returns {Promise<string>} Canonical level key
104114
*/
105115
async function resolveEligibleLevel(github, homeRepo, username) {
106116
for (const candidate of [...CONFIG.skillHierarchy].reverse()) {
107-
const eligible = await isEligibleFor(github, homeRepo, username, candidate);
108-
// false includes API failures (treated conservatively as ineligible)
117+
const eligible = await isEligibleFor(
118+
github,
119+
homeRepo,
120+
username,
121+
candidate,
122+
);
123+
124+
// false includes API failures (treated conservatively)
109125
if (eligible === true) return candidate;
110126
}
127+
111128
return CONFIG.skillHierarchy[0];
112129
}
113130

114131
/**
115-
* Adjusts the historical eligibility ceiling to include the current PR's completion.
132+
* Projects the contributor's eligibility ceiling after including
133+
* the current PR completion.
116134
*
117-
* resolveEligibleLevel is intentionally blind to the PR being processed.
118-
* Without this adjustment, a contributor completing their very first GFI
119-
* would have an eligibility ceiling of 'gfi' and never see beginner issues.
135+
* resolveEligibleLevel() intentionally ignores the current PR.
136+
* Without this adjustment, contributors completing their first GFI
137+
* would never receive beginner recommendations.
120138
*
121-
* Logic: if the ceiling is at or below the completed level, bump it one step up.
139+
* Logic:
140+
* if eligible ceiling <= completed level
141+
* => bump ceiling one level upward
122142
*
123-
* @param {string} completedKey - Canonical key of the level just completed.
124-
* @param {string} eligibleKey - Historical ceiling from resolveEligibleLevel.
125-
* @returns {string} Adjusted ceiling key.
143+
* @param {string} completedKey - Level completed by current PR
144+
* @param {string} eligibleKey - Historical eligibility ceiling
145+
* @returns {string} Adjusted eligibility ceiling
126146
*/
127147
function adjustEligibilityForCurrentPR(completedKey, eligibleKey) {
128-
const h = CONFIG.skillHierarchy;
148+
const h = CONFIG.skillHierarchy;
149+
129150
const completedIdx = h.indexOf(completedKey);
130-
const eligibleIdx = h.indexOf(eligibleKey);
151+
const eligibleIdx = h.indexOf(eligibleKey);
131152

132153
return eligibleIdx <= completedIdx
133154
? h[Math.min(completedIdx + 1, h.length - 1)]
134155
: eligibleKey;
135156
}
136157

137158
/**
138-
* Computes next level metadata for unlock detection.
159+
* Computes metadata about the next progression level.
139160
*
140161
* Ensures:
141162
* - current level exists
@@ -147,50 +168,73 @@ function adjustEligibilityForCurrentPR(completedKey, eligibleKey) {
147168
*/
148169
function getNextLevelInfo(currentLevelKey) {
149170
const hierarchy = CONFIG.skillHierarchy;
171+
150172
const currentIndex = hierarchy.indexOf(currentLevelKey);
151173
if (currentIndex === -1) return null;
152174

153175
const nextKey = hierarchy[currentIndex + 1];
154176
if (!nextKey) return null;
155177

156178
const nextPrereq = CONFIG.skillPrerequisites[nextKey];
157-
if (!nextPrereq || nextPrereq.requiredLevel !== currentLevelKey) return null;
179+
180+
if (
181+
!nextPrereq ||
182+
nextPrereq.requiredLevel !== currentLevelKey
183+
) {
184+
return null;
185+
}
158186

159187
return { nextKey, nextPrereq };
160188
}
161189

162190
/**
163-
* Detects if the contributor just unlocked the next level.
191+
* Detects whether the current PR completion unlocks the next level.
192+
*
193+
* GitHub search indexing may lag behind workflow execution, so counts are
194+
* treated as historical state and the current PR completion is projected
195+
* locally via +1.
164196
*
165-
* Trigger condition:
166-
* completed count === requiredCount
197+
* Example:
198+
* intermediate requires 3 beginner completions
199+
* historical beginner count = 2
200+
* current PR completes another beginner issue
201+
* projected count = 3 => unlock intermediate
167202
*
168-
* Uses a capped query (requiredCount + 1) for efficiency.
203+
* Uses a capped query for efficiency.
169204
*
170205
* @param {import('@actions/github').GitHub} github
171206
* @param {object} homeRepo
172207
* @param {string} username
173208
* @param {string} currentLevelKey
174209
* @returns {Promise<string|null>} unlocked level key
175210
*/
176-
async function detectUnlockedLevel(github, homeRepo, username, currentLevelKey) {
211+
async function detectUnlockedLevel(
212+
github,
213+
homeRepo,
214+
username,
215+
currentLevelKey,
216+
) {
177217
const next = getNextLevelInfo(currentLevelKey);
178218
if (!next) return null;
179219

180220
const { nextKey, nextPrereq } = next;
181221

182-
const count = await countClosedIssuesByAssignee(
222+
const historicalCount = await countClosedIssuesByAssignee(
183223
github,
184224
homeRepo.owner,
185225
homeRepo.repo,
186226
username,
187227
repoLabelFor(homeRepo, currentLevelKey),
188-
nextPrereq.requiredCount + 1,
228+
nextPrereq.requiredCount,
189229
);
190230

191-
if (count === null) return null;
231+
if (historicalCount === null) return null;
232+
233+
const projectedCount = historicalCount + 1;
192234

193-
return count === nextPrereq.requiredCount ? nextKey : null;
235+
return projectedCount === nextPrereq.requiredCount
236+
? nextKey
237+
: null;
194238
}
195239

196240
module.exports = {

0 commit comments

Comments
 (0)