From efd0e62e3e3da2ee497b981d510d9b6aa4347608 Mon Sep 17 00:00:00 2001 From: Mayank Bhaskar Date: Sun, 16 Aug 2026 09:10:11 +0000 Subject: [PATCH] fix(parser): retain single-letter language skills --- src/lib/heuristics/corpus.test.ts | 2 +- src/lib/heuristics/extract/skills.test.ts | 6 ++++++ src/lib/heuristics/extract/skills.ts | 7 +++++++ ...le-docs-skia-proxy-role-first-experience.expected.json | 6 +++--- ...oogle-docs-skia-proxy-role-first-experience.truth.json | 6 +++--- .../pdfs/latex/deedy-resume-macfonts.expected.json | 6 +++--- .../pdfs/latex/deedy-resume-openfonts.expected.json | 6 +++--- .../pdfs/latex/multi-degree-coursework.expected.json | 6 +++--- .../pdfs/latex/multi-degree-coursework.truth.json | 8 +------- .../pdflib-leading-glyph-skills-header.expected.json | 6 +++--- 10 files changed, 33 insertions(+), 26 deletions(-) diff --git a/src/lib/heuristics/corpus.test.ts b/src/lib/heuristics/corpus.test.ts index d33a440a..a5606054 100644 --- a/src/lib/heuristics/corpus.test.ts +++ b/src/lib/heuristics/corpus.test.ts @@ -183,7 +183,7 @@ const TRUTH_ANNOTATED_FIELD_FLOOR = 150; * `npm run check:baselines` on every run, and bounded here — undescribed debt may * not GROW. File the issue and flip the entry to `open`; then lower this. */ -const UNFILED_TRUTH_CEILING = 9; +const UNFILED_TRUTH_CEILING = 8; /** Generator category = the fixture root's immediate subdirectory. */ function categoryOf(repoRelPdfPath: string): string { diff --git a/src/lib/heuristics/extract/skills.test.ts b/src/lib/heuristics/extract/skills.test.ts index 515d9c43..3f3c5712 100644 --- a/src/lib/heuristics/extract/skills.test.ts +++ b/src/lib/heuristics/extract/skills.test.ts @@ -79,6 +79,12 @@ describe("tokenizeSkillLine", () => { expect(tokenizeSkillLine(",,,;;;")).toEqual([]); }); + it("keeps the defensible single-letter languages and rejects stray glyphs (#832)", () => { + const result = tokenizeSkillLine("C, R, D, X"); + expect(result).toEqual(expect.arrayContaining(["C", "R", "D"])); + expect(result).not.toContain("X"); + }); + it("drops the whole cell when a URL is present in a comma-separated list", () => { // tokenizeCell's looksLikeContactLink check fires on the ENTIRE cleaned // cell before the split. "github.com/janesmith" matches the path-slash diff --git a/src/lib/heuristics/extract/skills.ts b/src/lib/heuristics/extract/skills.ts index 724a6eed..fd2ecf43 100644 --- a/src/lib/heuristics/extract/skills.ts +++ b/src/lib/heuristics/extract/skills.ts @@ -130,6 +130,12 @@ const PROFILE_HOST_RE = * "Socket.io", "ASP.NET") that has no slash. */ const URLISH_RE = /(https?:\/\/|www\.|\b[a-z0-9-]+\.[a-z]{2,}\/\S)/i; +/** One-character tokens that are real, commonly-listed languages. The length + * floor in `isSkillToken` is a noise guard against stray glyphs left by column + * splitting; these are the only single characters that are not noise, so they + * are allowlisted rather than lowering the floor. */ +const SINGLE_LETTER_SKILLS = new Set(["c", "r", "d"]); + /** True when a candidate skill token is really a professional-profile link * (GitHub / LinkedIn / portfolio, etc.) or its bare heading word. Such links * belong only in the contact/profile section, never in Skills. */ @@ -139,6 +145,7 @@ function looksLikeContactLink(tok: string): boolean { } function isSkillToken(tok: string): boolean { + if (tok.length === 1 && SINGLE_LETTER_SKILLS.has(tok.toLowerCase())) return true; if (tok.length < 2 || tok.length > 40) return false; if (/^\d+$/.test(tok)) return false; // A professional-profile link (or its bare "GitHub" / "LinkedIn" heading) is diff --git a/tests/fixtures/pdfs/google-docs/google-docs-skia-proxy-role-first-experience.expected.json b/tests/fixtures/pdfs/google-docs/google-docs-skia-proxy-role-first-experience.expected.json index e1719859..62e33e1a 100644 --- a/tests/fixtures/pdfs/google-docs/google-docs-skia-proxy-role-first-experience.expected.json +++ b/tests/fixtures/pdfs/google-docs/google-docs-skia-proxy-role-first-experience.expected.json @@ -22,7 +22,7 @@ "phoneIsValid", "skills" ], - "skillsCount": 11, + "skillsCount": 12, "experienceCount": 2, "educationCount": 1, "projectsCount": 0, @@ -74,7 +74,7 @@ "sectionSource": "regex", "pageCount": 1, "rawCharCount": 1428, - "extractedCharCount": 1146, + "extractedCharCount": 1147, "sections": [ { "name": "profile", @@ -101,7 +101,7 @@ "hasSummary": false, "experienceCount": 2, "educationCount": 1, - "skillsCount": 11 + "skillsCount": 12 }, "linkAnnotationCount": 0, "disagreements": [] diff --git a/tests/fixtures/pdfs/google-docs/google-docs-skia-proxy-role-first-experience.truth.json b/tests/fixtures/pdfs/google-docs/google-docs-skia-proxy-role-first-experience.truth.json index f93e133e..fd40cccf 100644 --- a/tests/fixtures/pdfs/google-docs/google-docs-skia-proxy-role-first-experience.truth.json +++ b/tests/fixtures/pdfs/google-docs/google-docs-skia-proxy-role-first-experience.truth.json @@ -44,9 +44,9 @@ "note": "Role 2's employer line reads “Multicultural Engineering Program – State Polytechnic University”; `company` comes back as just the university — the program half is not lost, the parser puts it on `team`, but `experience.company` scores the `company` field alone. The identical shape is measured on unknown/single-column-title-below-anchor. Possibly a defensible org/team split rather than a defect — recorded rather than assumed, because ground truth's job is to state what the page says and let a human adjudicate." }, "skills": { - "issue": null, - "status": "unfiled", - "note": "Two independent disagreements on one field: the single-letter token “C” is DROPPED from the Programming Languages row (the identical drop is measured on latex/multi-degree-coursework, so it is not fixture-specific), and “Fluent in Spanish” is admitted as a skill from the “Language:” row." + "issue": 833, + "status": "open", + "note": "The independent disagreement — “Fluent in Spanish” is admitted as a skill from the “Language:” row — is tracked by #833. The separate single-letter “C” drop was fixed by #832." } } } diff --git a/tests/fixtures/pdfs/latex/deedy-resume-macfonts.expected.json b/tests/fixtures/pdfs/latex/deedy-resume-macfonts.expected.json index 791e8a77..1038d9c7 100644 --- a/tests/fixtures/pdfs/latex/deedy-resume-macfonts.expected.json +++ b/tests/fixtures/pdfs/latex/deedy-resume-macfonts.expected.json @@ -27,7 +27,7 @@ "skills", "website_url" ], - "skillsCount": 22, + "skillsCount": 23, "experienceCount": 6, "educationCount": 3, "projectsCount": 0, @@ -83,7 +83,7 @@ "sectionSource": "markdown", "pageCount": 1, "rawCharCount": 3205, - "extractedCharCount": 2156, + "extractedCharCount": 2157, "sections": [ { "name": "profile", @@ -114,7 +114,7 @@ "hasSummary": false, "experienceCount": 6, "educationCount": 3, - "skillsCount": 22 + "skillsCount": 23 }, "linkAnnotationCount": 9, "disagreements": [] diff --git a/tests/fixtures/pdfs/latex/deedy-resume-openfonts.expected.json b/tests/fixtures/pdfs/latex/deedy-resume-openfonts.expected.json index 97b56279..99d251e1 100644 --- a/tests/fixtures/pdfs/latex/deedy-resume-openfonts.expected.json +++ b/tests/fixtures/pdfs/latex/deedy-resume-openfonts.expected.json @@ -27,7 +27,7 @@ "skills", "website_url" ], - "skillsCount": 22, + "skillsCount": 23, "experienceCount": 6, "educationCount": 3, "projectsCount": 0, @@ -83,7 +83,7 @@ "sectionSource": "markdown", "pageCount": 1, "rawCharCount": 3207, - "extractedCharCount": 2158, + "extractedCharCount": 2159, "sections": [ { "name": "profile", @@ -114,7 +114,7 @@ "hasSummary": false, "experienceCount": 6, "educationCount": 3, - "skillsCount": 22 + "skillsCount": 23 }, "linkAnnotationCount": 9, "disagreements": [] diff --git a/tests/fixtures/pdfs/latex/multi-degree-coursework.expected.json b/tests/fixtures/pdfs/latex/multi-degree-coursework.expected.json index 97835aa6..a7c6204d 100644 --- a/tests/fixtures/pdfs/latex/multi-degree-coursework.expected.json +++ b/tests/fixtures/pdfs/latex/multi-degree-coursework.expected.json @@ -25,7 +25,7 @@ "website_url", "work_authorization" ], - "skillsCount": 17, + "skillsCount": 18, "experienceCount": 4, "educationCount": 2, "projectsCount": 3, @@ -77,7 +77,7 @@ "sectionSource": "markdown", "pageCount": 1, "rawCharCount": 3428, - "extractedCharCount": 2625, + "extractedCharCount": 2626, "sections": [ { "name": "profile", @@ -108,7 +108,7 @@ "hasSummary": false, "experienceCount": 4, "educationCount": 2, - "skillsCount": 17 + "skillsCount": 18 }, "linkAnnotationCount": 6, "disagreements": [] diff --git a/tests/fixtures/pdfs/latex/multi-degree-coursework.truth.json b/tests/fixtures/pdfs/latex/multi-degree-coursework.truth.json index 12c6706a..b8aa2cce 100644 --- a/tests/fixtures/pdfs/latex/multi-degree-coursework.truth.json +++ b/tests/fixtures/pdfs/latex/multi-degree-coursework.truth.json @@ -58,11 +58,5 @@ "Raspberry Pi", "iOS" ], - "knownWrong": { - "skills": { - "issue": null, - "status": "unfiled", - "note": "The single-letter token “C” is DROPPED from the Languages row while “C++” survives. Second independent measurement of the same drop (see google-docs/google-docs-skia-proxy-role-first-experience)." - } - } + "knownWrong": {} } diff --git a/tests/fixtures/pdfs/unknown/pdflib-leading-glyph-skills-header.expected.json b/tests/fixtures/pdfs/unknown/pdflib-leading-glyph-skills-header.expected.json index 0dbe7d27..3eab12ad 100644 --- a/tests/fixtures/pdfs/unknown/pdflib-leading-glyph-skills-header.expected.json +++ b/tests/fixtures/pdfs/unknown/pdflib-leading-glyph-skills-header.expected.json @@ -22,7 +22,7 @@ "phoneIsValid", "skills" ], - "skillsCount": 8, + "skillsCount": 9, "experienceCount": 1, "educationCount": 1, "projectsCount": 0, @@ -74,7 +74,7 @@ "sectionSource": "markdown", "pageCount": 1, "rawCharCount": 462, - "extractedCharCount": 289, + "extractedCharCount": 290, "sections": [ { "name": "profile", @@ -101,7 +101,7 @@ "hasSummary": false, "experienceCount": 1, "educationCount": 1, - "skillsCount": 8 + "skillsCount": 9 }, "linkAnnotationCount": 0, "disagreements": []