Problem
src/lib/heuristics/extract-fields.ts is 1060 LOC — the largest module in the repo — and holds all 10 field extractors plus ~20 private helpers in one file:
extractName (l.174)
extractContact (l.292) + ContactExtractionResult interface
extractSummary (l.496)
extractSkills (l.538)
extractExperience (l.575)
extractProjects (l.650)
extractAchievements (l.744)
disambiguateCompanyTitle (l.932)
extractEducation (l.988)
Each extractor is now independently complex (name scoring, contact band/annotation logic, education chunking, experience entry-blocking, column-skill splitting). The file has ~15 issue/template-specific heuristic anchors and keeps growing — PR #125 alone added +400 lines to it. This is a god-module: hard to navigate, hard to review a single extractor's change in isolation, and a magnet for unrelated churn.
This is a behavior-preserving refactor — no parsing logic changes, no score changes, no snapshot changes.
Why now
The deterministic Tier-1 extractor is where most template-fix PRs land, so the file's growth rate is high. Splitting now (before more accretion) keeps each extractor reviewable and makes the next round of fixes touch a ~150-LOC file instead of a 1060-LOC one.
Current structure facts (verified)
- Sole non-test importer:
src/lib/heuristics/openresume.ts:44 imports extractName, extractContact, extractSummary, extractSkills, extractExperience, extractProjects, extractAchievements, extractEducation from ./extract-fields.ts. (disambiguateCompanyTitle is exported but consumed internally by extractExperience.)
- Cross-extractor private helpers:
looksLikeContactLink — used by extractSkills (token + cell rejection).
disambiguateCompanyTitle — used by extractExperience (and exported).
parseEducationDates / educationDateFields — education-only.
isBulletLine, stripBullet (imported from ./sections.ts), mergeItemText (imported from ./sections.ts), matchSectionHeader (from ./regex.ts) — already external, stay as imports.
Implementation plan
- Create per-field modules under
src/lib/heuristics/extract/ (new dir):
name.ts — extractName + isSingleNameWord, looksLikeMononymName, findContactClusterY, looksLikeDocTitleBoilerplate.
contact.ts — extractContact, ContactExtractionResult, isLinkedinProfileUrl, LINKEDIN_NONPROFILE_RE, normalizeUrl, annotation/band helpers.
summary.ts — extractSummary.
skills.ts — extractSkills + isSkillToken, splitColumnCells, looksLikeContactLink, PROFILE_LABEL_RE/PROFILE_HOST_RE/URLISH_RE.
experience.ts — extractExperience, disambiguateCompanyTitle, looksLikeTitle, looksLikeCompany, TITLE_KEYWORDS_RE.
education.ts — extractEducation, educationFromChunk, isDateOnlyLine, parseEducationDates, educationDateFields.
projects.ts — extractProjects.
achievements.ts — extractAchievements.
- Shared helper placement: put a helper in the one module that uses it. If two modules need one (e.g.
looksLikeContactLink if it ever spreads), promote it to a small extract/shared.ts — but only on real second use, not speculatively.
- Keep
src/lib/heuristics/extract-fields.ts as a barrel that re-exports the public surface, so openresume.ts:44 imports are unchanged:
export { extractName } from "./extract/name.ts";
export { extractContact, type ContactExtractionResult } from "./extract/contact.ts";
// …etc
export { disambiguateCompanyTitle } from "./extract/experience.ts";
(Optionally update openresume.ts to import from the new paths and delete the barrel — but the barrel is the lower-risk default; decide during implementation.)
- Move the co-located tests the same way, or keep
extract-fields.test.ts importing through the barrel — either is fine as long as the suite stays green.
- Preserve SPDX headers on every new file (the 3-line Apache-2.0 header).
- Run
npm run typecheck && npm run test && npm run lint.
Acceptance criteria
Out of scope
- Any change to extraction behavior (that's the heuristic PRs' lane).
- The scorer / segmentation rework (tracked separately — see the section-segmentation issue).
Problem
src/lib/heuristics/extract-fields.tsis 1060 LOC — the largest module in the repo — and holds all 10 field extractors plus ~20 private helpers in one file:Each extractor is now independently complex (name scoring, contact band/annotation logic, education chunking, experience entry-blocking, column-skill splitting). The file has ~15 issue/template-specific heuristic anchors and keeps growing — PR #125 alone added +400 lines to it. This is a god-module: hard to navigate, hard to review a single extractor's change in isolation, and a magnet for unrelated churn.
This is a behavior-preserving refactor — no parsing logic changes, no score changes, no snapshot changes.
Why now
The deterministic Tier-1 extractor is where most template-fix PRs land, so the file's growth rate is high. Splitting now (before more accretion) keeps each extractor reviewable and makes the next round of fixes touch a ~150-LOC file instead of a 1060-LOC one.
Current structure facts (verified)
src/lib/heuristics/openresume.ts:44importsextractName, extractContact, extractSummary, extractSkills, extractExperience, extractProjects, extractAchievements, extractEducationfrom./extract-fields.ts. (disambiguateCompanyTitleis exported but consumed internally byextractExperience.)looksLikeContactLink— used byextractSkills(token + cell rejection).disambiguateCompanyTitle— used byextractExperience(and exported).parseEducationDates/educationDateFields— education-only.isBulletLine,stripBullet(imported from./sections.ts),mergeItemText(imported from./sections.ts),matchSectionHeader(from./regex.ts) — already external, stay as imports.Implementation plan
src/lib/heuristics/extract/(new dir):name.ts—extractName+isSingleNameWord,looksLikeMononymName,findContactClusterY,looksLikeDocTitleBoilerplate.contact.ts—extractContact,ContactExtractionResult,isLinkedinProfileUrl,LINKEDIN_NONPROFILE_RE,normalizeUrl, annotation/band helpers.summary.ts—extractSummary.skills.ts—extractSkills+isSkillToken,splitColumnCells,looksLikeContactLink,PROFILE_LABEL_RE/PROFILE_HOST_RE/URLISH_RE.experience.ts—extractExperience,disambiguateCompanyTitle,looksLikeTitle,looksLikeCompany,TITLE_KEYWORDS_RE.education.ts—extractEducation,educationFromChunk,isDateOnlyLine,parseEducationDates,educationDateFields.projects.ts—extractProjects.achievements.ts—extractAchievements.looksLikeContactLinkif it ever spreads), promote it to a smallextract/shared.ts— but only on real second use, not speculatively.src/lib/heuristics/extract-fields.tsas a barrel that re-exports the public surface, soopenresume.ts:44imports are unchanged:openresume.tsto import from the new paths and delete the barrel — but the barrel is the lower-risk default; decide during implementation.)extract-fields.test.tsimporting through the barrel — either is fine as long as the suite stays green.npm run typecheck && npm run test && npm run lint.Acceptance criteria
extract-fields.tsis ≤ ~60 LOC (barrel re-exports only) or removed withopenresume.tsimporting the new modules directly.npm run testgreen with zero changes to any*.expected.jsonsnapshot (proves behavior-preserving).npm run typecheckandnpm run lintclean.openresume.tsproduces byte-identicalHeuristicResultfor the existing fixture corpus (snapshot stability is the proof)..tsfile carries the SPDX 3-line header.Out of scope