Issue #432: Bug: role.json min_final_score / max_final_score are loaded but never applied to the final score
No open PR fixes this and merges clean
0 fix it but don't merge clean, 2 fix it partly. See the board below.
What's wrong
The report is confirmed. `min_final_score` and `max_final_score` are parsed into `Role` in roles.py, but nothing outside roles.py reads them. `print_evaluation_results` in score.py computes the total as category scores + bonus − deductions.Show full explanation
It caps the total only at `sum(category max) + role.bonus_max`, which is a value it recomputes on its own, and it has no lower bound. `Deductions.total` is only constrained by `ge=0`, so a large deduction can push the total below `min_final_score`. A role author who sets `max_final_score` to something other than `sum(categories) + bonus_max` sees no effect. The CSV path in transform.py is also unclamped: its `total_score` sums only the category scores and leaves out bonus and deductions. One detail differs from the report: the intern role.json in this checkout has `max_final_score` 130, not 120.How to reproduce
- Use the software_engineering_intern role, whose role.json sets min_final_score to -20.
- Produce or mock an evaluation whose category scores are low and whose deductions.total is large, for example 100. Deductions.total has no upper bound, only ge=0.
- Call print_evaluation_results(evaluation, role, ...) from score.py. The printed OVERALL SCORE is below -20 (for example -60). Nothing raises the total to min_final_score.
- Edit role.json to set max_final_score to a value lower than sum(category max) + bonus_max, for example 50, and use an evaluation that scores high. The printout is still capped at max_score + bonus_max, not at 50.
- Run `grep -rn 'min_final_score\|max_final_score' --include=*.py .`. Only roles.py matches. score.py and transform.py never reference these fields.
Evidence
Each citation was checked against the code at e0ece80.
The field is parsed into Role here. The grep found no read of it anywhere else in the Python code.
max_final_score is parsed here. Its default is sum(category max) + bonus_max, and nothing else uses it.
Deductions are subtracted with no lower bound or floor.
The cap is recomputed from category maxima plus bonus_max instead of using role.max_final_score.
Only an upper clamp exists. There is no comparison against role.min_final_score.
Deductions.total has only a lower bound, so deductions can be arbitrarily large.
The CSV total sums only the category scores and applies no bonus, deductions, or min/max clamp.
The intern role configures a floor that is never enforced.
Pull requests
Verdicts are judged from the diff, not run. Merge status is a real git check against e0ece80.
It adds a lower-bound clamp and puts bonus and deductions into the CSV total, but it uses hardcoded MIN/MAX_FINAL_SCORE constants from evaluator instead of role.min_final_score and role.max_final_score, so role.json settings still have no effect.
It clamps the print_evaluation_results total to [role.min_final_score, role.max_final_score], which removes the missing floor and the recomputed cap, but it doesn't touch transform.py, so the CSV total still sums only category scores with no bonus, deductions or clamp.
16 PRs that don't fix itShow
It migrates GeminiProvider to the google-genai SDK and doesn't touch scoring.
It adds an optional job_match score and job description plumbing, and leaves the cap and floor logic unchanged.
It adds PDF and resume validation before the LLM call and leaves the hardcoded 'max_score + 20' cap with no floor and no use of the role bounds.
It adds a scholarship_mentorship category and provider fixes, and leaves the final-score cap and floor logic unchanged.
It adds Gemini structured output and strips pydantic constraints from the models; it doesn't enforce min or max final score anywhere.
Its visible diff is the same models.py structured-output change (constraints removed); it adds no final-score clamping.
It adds an optional role_fit category and CSV columns, and leaves the final-score cap and floor logic unchanged.
It adds Codeforces, LeetCode and token-usage plumbing and a problem_solving category, and leaves the final-score cap and floor logic unchanged.
It clamps individual LLM fields (deductions, bonus, category score) before validation so the run doesn't crash, and it doesn't apply the role's final-score bounds to the total.
It adds per-experience-level category maxima in roles.py and removes the hardcoded category_maxes dict, and doesn't touch final-score clamping or the CSV total.
It adds a competitive_programming category and raises the hardcoded bonus cap comment to 140, and leaves the cap and floor logic as it was.
It is a refactor into calculate_total_score that keeps the same 'max + 20' cap, adds no floor, and leaves the CSV path alone.
It moves the arithmetic into compute_totals but keeps the 'max_score + role.bonus_max' cap, and nothing in the diff reads role.min_final_score or role.max_final_score.
It only switches the category cap and denominators to role.categories[*].max and never reads min_final_score or max_final_score, and it adds no lower bound or bonus/deduction handling to the CSV total.
It validates min/max_final_score types and ordering in load_role, but nothing in the scoring path starts reading those fields, so they remain unenforced.
It adds an optional evidence-trace feature with citations and reports, and leaves the total computation and clamping unchanged.
What this packet does not prove
- Citations are checked against the code. The explanation that links them, the order of the chain, and any claim about outside systems (APIs, libraries) are the model's reasoning and are not checked.
- PR verdicts are judged from the diff, not run. No PR was applied or tested.
- PRs were shortlisted because they change a cited line or mention the issue. A fix in another file, or a PR whose line numbers have drifted, is missed, so this is not proof that no other PR fixes it.