diff --git a/src/lib/heuristics/corpus.test.ts b/src/lib/heuristics/corpus.test.ts index d33a440a..a549a1c9 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 = 7; /** 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..47bab79b 100644 --- a/src/lib/heuristics/extract/skills.test.ts +++ b/src/lib/heuristics/extract/skills.test.ts @@ -402,8 +402,8 @@ describe("tokenizeSkillLine — issue #221 non-skill sub-labels", () => { }); it("keeps skill sub-labels (Languages/Technologies/Tools/Frameworks)", () => { - expect(tokenizeSkillLine("Languages: Python, Go, C++, Java")).toEqual( - expect.arrayContaining(["Python", "Go", "C++", "Java"]), + expect(tokenizeSkillLine("Languages: Python, Go, C++, Java, C")).toEqual( + expect.arrayContaining(["Python", "Go", "C++", "Java", "C"]), ); expect(tokenizeSkillLine("Technologies: Linux, AWS, Docker, iOS")).toEqual( expect.arrayContaining(["Linux", "AWS", "Docker", "iOS"]), @@ -426,6 +426,23 @@ describe("tokenizeSkillLine — issue #221 non-skill sub-labels", () => { }); }); +describe("tokenizeSkillLine — issue #832 single-letter languages", () => { + it("keeps C, R, and D as real programming languages", () => { + expect(tokenizeSkillLine("Languages: C, R, D")).toEqual( + expect.arrayContaining(["C", "R", "D"]), + ); + }); + + it("still rejects other single-character noise tokens", () => { + expect(tokenizeSkillLine("Skills: x, J, •, (")).toEqual([]); + }); + + it("allows allowlisted single letters alongside real skills on the same line", () => { + // Positive control: distinguishes rejecting noise from rejecting everything. + expect(tokenizeSkillLine("Skills: Python, x, J, C")).toEqual(["Python", "C"]); + }); +}); + describe("parseHeuristic — issue #221 Interests sub-label in SKILLS section", () => { it("excludes Interests items while keeping Languages/Technologies skills", () => { // Repro from the issue: a Technical Skills section internally sub-labeled diff --git a/src/lib/heuristics/extract/skills.ts b/src/lib/heuristics/extract/skills.ts index 724a6eed..1c719a41 100644 --- a/src/lib/heuristics/extract/skills.ts +++ b/src/lib/heuristics/extract/skills.ts @@ -138,7 +138,14 @@ function looksLikeContactLink(tok: string): boolean { return PROFILE_LABEL_RE.test(t) || PROFILE_HOST_RE.test(t) || URLISH_RE.test(t); } +/** One-character tokens that are real, commonly-listed languages. The + * `tok.length < 2` 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"]); + 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..163c6651 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": "“Fluent in Spanish” is admitted as a skill from the “Language:” row; the Programming Languages row is now correct after #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..5c9fe1a2 100644 --- a/tests/fixtures/pdfs/latex/multi-degree-coursework.truth.json +++ b/tests/fixtures/pdfs/latex/multi-degree-coursework.truth.json @@ -57,12 +57,5 @@ "Docker", "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)." - } - } + ] } 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": []