fix(year-level): prefer units-passed standing over calendar-year history count
ci / test (push) Successful in 2m21s
ci / test (pull_request) Successful in 38s

The grade-history method counted distinct academic years, which over-advanced
standing for students who repeated/failed a year (e.g. showing Year 5 when the
student is genuinely Year 4, since a partial 2025 year didn't advance them).

Reorder the hybrid so the UNITS math (total prospectus units minus still-lacking
units = units actually passed) is primary, and View Grades history is only a
fallback when units data is unavailable.
This commit is contained in:
2026-08-20 08:54:55 +08:00
parent 3067335274
commit 90e4107bf0
2 changed files with 21 additions and 15 deletions
+14 -9
View File
@@ -179,15 +179,10 @@
const yearNum = parseInt(year, 10);
// 1. View Grades history matching.
const gradesHtml = await fetchText('/ViewGrades');
if (gradesHtml) {
const history = ISMIStools.parseGradeHistory(gradesHtml);
const level = ISMIStools.yearLevelForSemester(history, yearNum);
if (level != null) return String(level);
}
// 2. Units math: prospectus total minus lacking units.
// 1. UNITS MATH — primary. Standing from units actually passed:
// total prospectus units minus still-lacking units. This reflects real
// earned units and is correct even for students who repeated or failed
// a year (the naive grade-history count would over-advance them).
const [prospectusHtml, lackingHtml] = await Promise.all([
fetchText('/prospectus/StudentProspectus'),
fetchText('/Extras/ViewLackingSubjectsStudent1')
@@ -199,6 +194,16 @@
if (level != null) return String(level);
}
// 2. GRADE HISTORY — fallback only when the units path is unavailable.
// It counts distinct academic years, so it is only an approximation for
// the printed semester and is not used when units data is present.
const gradesHtml = await fetchText('/ViewGrades');
if (gradesHtml) {
const history = ISMIStools.parseGradeHistory(gradesHtml);
const level = ISMIStools.yearLevelForSemester(history, yearNum);
if (level != null) return String(level);
}
// 3. Profile-scraped value as a last resort.
if (header.yearLevel) return String(header.yearLevel);
return null;