Problem
GPA and Latin honors are silently dropped on parse. They never reach the canonical résumé, so they are absent from the reconstructed résumé, from the export, from the JSON Resume attachment, and from JD matching. A source entry like:
The University of Texas at Dallas May 2024
B.S. in Computer Science, cum laude, GPA: 3.72/4.00
round-trips to a degree and an institution. cum laude and 3.72/4.00 are gone, with no flag and no finding — the user is not told anything was lost.
Evidence
ResumeEducation (src/lib/score/types.ts) has no gpa and no honors field.
- It does have
description?, but nothing writes it: src/lib/heuristics/extract/education.ts never assigns a description, and openresume.ts passes extractEducation's value straight through. The field is read (src/lib/jd-match/coverage.ts folds it into coverage text) and never populated — a dead input.
GPA appears in extract/education.ts only as a negative signal: isEntryLead and isInlineDatedProgram both reject a line matching /^(GPA[:\s]|Minor\b|Major\b)/i so it does not split off a phantom entry. Correct as far as it goes — the line is stopped from becoming an entry, but nothing then collects it.
src/lib/pdf/ats-resume-model.ts builds an education entry's bullets from edu.coursework only, so even a populated description would not reach the PDF.
So this is not a rendering gap. The data is discarded at Tier 1.
Why it matters beyond the export
GPA is a screening field. A new-grad résumé that states a 3.9 and re-exports without it is materially weaker, and the anonymous score has no visibility into it either. Latin honors is the same class of signal. Both are stated by the candidate on the page in front of us; dropping them is data loss the user did not ask for.
Proposal
- Model — add
gpa?: string and honors?: string to ResumeEducation. Keep GPA a string, not a number: 3.72/4.00, 3.9/4.0, 8.4/10, First Class and 2:1 are all real, and normalising to a float throws away the scale and invents precision. Store what the résumé said.
- Extract — collect them off the degree line and off a standalone annotation line, at the same place the existing
/^(GPA[:\s]|Minor\b|Major\b)/i guards already recognise the shape, so recognition lives in one predicate rather than two that can drift. Honors: the Latin set plus with honors / with distinction.
- Edit — expose both as
EditableFields on the education entry in ReconstructedEducationSkills.tsx, with the "+ add" affordance the other cleared fields use, so a parse that missed one is fixable.
- Export — compose the sub-line as
<degree>, <honors>, GPA: <gpa>, matching the shape the source used. Round-trip: the composed line must re-parse back into the same three fields.
- JSON Resume —
education[].score carries GPA in the spec; map it in to-json-resume.ts. Honors has no spec field; do not invent one.
Open question for whoever picks this up
Whether description should be deleted or populated. It is currently a field the model declares, nothing writes, and one consumer reads — which is worse than not having it, because coverage.ts looks like it handles education prose and in fact never can. Decide one way; do not leave it as is.
Acceptance criteria
Problem
GPA and Latin honors are silently dropped on parse. They never reach the canonical résumé, so they are absent from the reconstructed résumé, from the export, from the JSON Resume attachment, and from JD matching. A source entry like:
round-trips to a degree and an institution.
cum laudeand3.72/4.00are gone, with no flag and no finding — the user is not told anything was lost.Evidence
ResumeEducation(src/lib/score/types.ts) has nogpaand nohonorsfield.description?, but nothing writes it:src/lib/heuristics/extract/education.tsnever assigns a description, andopenresume.tspassesextractEducation's value straight through. The field is read (src/lib/jd-match/coverage.tsfolds it into coverage text) and never populated — a dead input.GPAappears inextract/education.tsonly as a negative signal:isEntryLeadandisInlineDatedProgramboth reject a line matching/^(GPA[:\s]|Minor\b|Major\b)/iso it does not split off a phantom entry. Correct as far as it goes — the line is stopped from becoming an entry, but nothing then collects it.src/lib/pdf/ats-resume-model.tsbuilds an education entry's bullets fromedu.courseworkonly, so even a populateddescriptionwould not reach the PDF.So this is not a rendering gap. The data is discarded at Tier 1.
Why it matters beyond the export
GPA is a screening field. A new-grad résumé that states a 3.9 and re-exports without it is materially weaker, and the anonymous score has no visibility into it either. Latin honors is the same class of signal. Both are stated by the candidate on the page in front of us; dropping them is data loss the user did not ask for.
Proposal
gpa?: stringandhonors?: stringtoResumeEducation. Keep GPA a string, not a number:3.72/4.00,3.9/4.0,8.4/10,First Classand2:1are all real, and normalising to a float throws away the scale and invents precision. Store what the résumé said./^(GPA[:\s]|Minor\b|Major\b)/iguards already recognise the shape, so recognition lives in one predicate rather than two that can drift. Honors: the Latin set pluswith honors/with distinction.EditableFields on the education entry inReconstructedEducationSkills.tsx, with the "+ add" affordance the other cleared fields use, so a parse that missed one is fixable.<degree>, <honors>, GPA: <gpa>, matching the shape the source used. Round-trip: the composed line must re-parse back into the same three fields.education[].scorecarries GPA in the spec; map it into-json-resume.ts. Honors has no spec field; do not invent one.Open question for whoever picks this up
Whether
descriptionshould be deleted or populated. It is currently a field the model declares, nothing writes, and one consumer reads — which is worse than not having it, becausecoverage.tslooks like it handles education prose and in fact never can. Decide one way; do not leave it as is.Acceptance criteria
GPA: 3.72/4.00andcum laudeon a degree line parse intogpa/honorsand survive parse → export → re-parse unchanged.GPA: 3.9/4.0annotation line under an entry is captured too, and still does not split off a phantom entry.8.4/10), bare values (3.9), and classifications (First Class,2:1) are preserved verbatim, not normalised.education[].scorein the JSON Resume export.descriptionis resolved — either populated or removed along with itscoverage.tsread.KNOWN_FAILURESrows.npm run verifyexits 0.