Problem
The openresume-laverne-word-quartz corpus fixture exposes a scoring inconsistency for student and volunteer-heavy resumes:
cascade.experienceCount → 0 (parsed.experience[].length)
score.bulletCount → 6 (extractBulletsFromText over rawText)
score.completeness.missing → ["work experience", "phone", "LinkedIn"]
The resume contains sections headed "VOLUNTEER EXPERIENCE" and "ON CAMPUS INVOLVEMENT" with 6 experience-shaped bullet points. The parser scores those 6 bullets for Specificity and Structure (document-scoped), but experienceCount stays 0 and Completeness deducts a full check for missing "work experience" — because experience extraction is section-scoped on SECTION_KEYWORDS.experience (regex.ts:96–105) while bullet counting runs document-scoped in extractBulletsFromText (score.ts:508).
Current snapshot: tests/fixtures/pdfs/word/openresume-laverne-word-quartz.expected.json
"experienceCount": 0,
"bulletCount": 6,
"completeness": {
"missing": ["LinkedIn", "phone", "work experience"]
}
Root cause
Two layers in the pipeline treat "experience" differently:
-
Tier 1 parser (openresume.ts:107): findSection(sections, "experience") returns null because matchSectionHeader (regex.ts:172) tests each heading against SECTION_KEYWORDS.experience (regex.ts:96–105). That list contains only canonical professional-experience synonyms ("experience", "work experience", "professional experience", "employment", "employment history", "work history", "career", "career history"). Headings like "VOLUNTEER EXPERIENCE" and "ON CAMPUS INVOLVEMENT" match nothing, so extractExperience(null) returns [] and parsed.experience stays empty.
-
Anonymous scorer (score.ts:530): extractBulletsFromText(input.rawText) scans the raw text string without any section boundary awareness. It finds all 6 bullet lines regardless of their parent heading. The Completeness check at score.ts:567–571 tests expEntries.length > 0 (where expEntries = input.parsed.experience ?? []), labels the failure "work experience", and deducts a full check slot.
The three keywords "volunteer", "volunteering", and "activities" currently live in SECTION_KEYWORDS.other (regex.ts:123–139), whose sole purpose is to act as a section boundary terminator — nothing renders an other section. Headings containing those words terminate the preceding section but do not open an experience section.
Decided fix — Compound A + C
Decision: student/volunteer headings count toward experienceCount AND drop the "work experience" Completeness deduction when bulletCount > 0.
Part A — Expand SECTION_KEYWORDS.experience (regex.ts:96–105)
Add student and non-standard synonyms so matchSectionHeader routes those headings into the experience section:
"volunteer experience"
"community service"
"leadership"
"involvement"
"on campus involvement"
"campus involvement"
"extracurricular"
"extracurricular activities"
"activities" ← moved from other
"volunteer" ← moved from other
"volunteering" ← moved from other
Keyword move: "volunteer", "volunteering", and "activities" are removed from SECTION_KEYWORDS.other and added to SECTION_KEYWORDS.experience. They previously served as boundary terminators; after the move they open an experience section instead. Accept this trade-off (see Risks).
Part C — Adjust the Completeness scorer (score.ts:567–571)
De-emphasize the "work experience" check when the document has bullets but no parsed experience entries — indicating that extraction missed content rather than that the resume has none:
// score.ts ~line 567
completenessChecks.push({
key: "experience",
passed: expEntries.length > 0 || bullets.length > 0,
label: "work experience",
});
This lets the bullet signal (already computed at line 530) serve as a fallback pass condition for the experience completeness check. The scorer still rewards resumes that produce parsed experience entries (enabling the dates sub-check at lines 580–587), but stops penalizing resumes where extraction failed to section-scope the content.
Acceptance criteria
- After Part A:
experienceCount ≥ 1 for the laverne fixture (at least one of "VOLUNTEER EXPERIENCE" or "ON CAMPUS INVOLVEMENT" is parsed into parsed.experience).
- After Part C:
"work experience" is absent from score.completeness.missing for the laverne fixture.
- The laverne
expected.json snapshot is updated to reflect both changes:
"experienceCount": ≥ 1
"work experience" removed from completeness.missing
- Overall score will increase (exact value to be captured in the updated snapshot run).
- Existing fixtures whose
experienceCount > 0 are unaffected — the keyword expansion only adds new match paths, it does not change existing ones.
- The
google-docs-skia-proxy-nonstandard-headers and weasyprint-cairo-nonstandard-headers fixtures (which also have experienceCount: 0 and "work experience" in missing) should be evaluated for whether their headings now match; if they do, those snapshots update too. If they don't, that confirms the fix scope is bounded.
npm run test passes (including corpus snapshot verification) with UPDATE=1 re-run to bake the new snapshots.
Risks
Two-column boundary bleed. "volunteer", "volunteering", and "activities" previously terminated the preceding section in SECTION_KEYWORDS.other. Moving them to experience means a two-column resume with a sidebar "VOLUNTEER" label will now open an experience section instead of closing the preceding one, potentially bleeding sidebar content into the experience entries. Mitigations:
- The
isTwoColumn layout probe (from LayoutProbes) is already in scope; a follow-up could gate the expanded keywords behind a !isTwoColumn condition if bleed is observed in practice.
- The existing two-column corpus fixtures (
google-docs-skia-proxy-two-column, weasyprint-cairo-two-column, chromium-asymmetric-sidebar) must be re-run with UPDATE=1 and their snapshots eyeballed for regression before merging.
False-positive "leadership" / "involvement". A resume that uses "Leadership" as a sidebar widget label rather than a section heading could bleed into experience. The split-letter recovery already in matchSectionHeader (regex.ts:172) does not change this risk; it applies equally to any added keyword.
Problem
The
openresume-laverne-word-quartzcorpus fixture exposes a scoring inconsistency for student and volunteer-heavy resumes:The resume contains sections headed "VOLUNTEER EXPERIENCE" and "ON CAMPUS INVOLVEMENT" with 6 experience-shaped bullet points. The parser scores those 6 bullets for Specificity and Structure (document-scoped), but
experienceCountstays 0 and Completeness deducts a full check for missing"work experience"— because experience extraction is section-scoped onSECTION_KEYWORDS.experience(regex.ts:96–105) while bullet counting runs document-scoped inextractBulletsFromText(score.ts:508).Current snapshot:
tests/fixtures/pdfs/word/openresume-laverne-word-quartz.expected.jsonRoot cause
Two layers in the pipeline treat "experience" differently:
Tier 1 parser (
openresume.ts:107):findSection(sections, "experience")returnsnullbecausematchSectionHeader(regex.ts:172) tests each heading againstSECTION_KEYWORDS.experience(regex.ts:96–105). That list contains only canonical professional-experience synonyms ("experience","work experience","professional experience","employment","employment history","work history","career","career history"). Headings like "VOLUNTEER EXPERIENCE" and "ON CAMPUS INVOLVEMENT" match nothing, soextractExperience(null)returns[]andparsed.experiencestays empty.Anonymous scorer (
score.ts:530):extractBulletsFromText(input.rawText)scans the raw text string without any section boundary awareness. It finds all 6 bullet lines regardless of their parent heading. The Completeness check at score.ts:567–571 testsexpEntries.length > 0(whereexpEntries = input.parsed.experience ?? []), labels the failure"work experience", and deducts a full check slot.The three keywords
"volunteer","volunteering", and"activities"currently live inSECTION_KEYWORDS.other(regex.ts:123–139), whose sole purpose is to act as a section boundary terminator — nothing renders anothersection. Headings containing those words terminate the preceding section but do not open anexperiencesection.Decided fix — Compound A + C
Decision: student/volunteer headings count toward
experienceCountAND drop the "work experience" Completeness deduction whenbulletCount > 0.Part A — Expand
SECTION_KEYWORDS.experience(regex.ts:96–105)Add student and non-standard synonyms so
matchSectionHeaderroutes those headings into the experience section:Keyword move:
"volunteer","volunteering", and"activities"are removed fromSECTION_KEYWORDS.otherand added toSECTION_KEYWORDS.experience. They previously served as boundary terminators; after the move they open an experience section instead. Accept this trade-off (see Risks).Part C — Adjust the Completeness scorer (score.ts:567–571)
De-emphasize the
"work experience"check when the document has bullets but no parsed experience entries — indicating that extraction missed content rather than that the resume has none:This lets the bullet signal (already computed at line 530) serve as a fallback pass condition for the experience completeness check. The scorer still rewards resumes that produce parsed
experienceentries (enabling thedatessub-check at lines 580–587), but stops penalizing resumes where extraction failed to section-scope the content.Acceptance criteria
experienceCount ≥ 1for the laverne fixture (at least one of "VOLUNTEER EXPERIENCE" or "ON CAMPUS INVOLVEMENT" is parsed intoparsed.experience)."work experience"is absent fromscore.completeness.missingfor the laverne fixture.expected.jsonsnapshot is updated to reflect both changes:"experienceCount": ≥ 1"work experience"removed fromcompleteness.missingexperienceCount > 0are unaffected — the keyword expansion only adds new match paths, it does not change existing ones.google-docs-skia-proxy-nonstandard-headersandweasyprint-cairo-nonstandard-headersfixtures (which also haveexperienceCount: 0and"work experience"in missing) should be evaluated for whether their headings now match; if they do, those snapshots update too. If they don't, that confirms the fix scope is bounded.npm run testpasses (including corpus snapshot verification) withUPDATE=1re-run to bake the new snapshots.Risks
Two-column boundary bleed.
"volunteer","volunteering", and"activities"previously terminated the preceding section inSECTION_KEYWORDS.other. Moving them toexperiencemeans a two-column resume with a sidebar "VOLUNTEER" label will now open an experience section instead of closing the preceding one, potentially bleeding sidebar content into the experience entries. Mitigations:isTwoColumnlayout probe (fromLayoutProbes) is already in scope; a follow-up could gate the expanded keywords behind a!isTwoColumncondition if bleed is observed in practice.google-docs-skia-proxy-two-column,weasyprint-cairo-two-column,chromium-asymmetric-sidebar) must be re-run withUPDATE=1and their snapshots eyeballed for regression before merging.False-positive
"leadership"/"involvement". A resume that uses "Leadership" as a sidebar widget label rather than a section heading could bleed into experience. The split-letter recovery already inmatchSectionHeader(regex.ts:172) does not change this risk; it applies equally to any added keyword.