diff --git a/src/job_research/profile/cv_extractor.py b/src/job_research/profile/cv_extractor.py index 7800bf9..25c5b76 100644 --- a/src/job_research/profile/cv_extractor.py +++ b/src/job_research/profile/cv_extractor.py @@ -35,6 +35,21 @@ EXPERIENCE_TITLE_STOPWORDS = { "because", } +EXPERIENCE_TITLE_ACTION_STARTS = { + "built", + "created", + "delivered", + "designed", + "developed", + "deployed", + "implemented", + "improved", + "managed", + "migrated", + "maintained", + "worked", +} + EXPERIENCE_TITLE_LABELS = { "education", "experience", @@ -79,6 +94,16 @@ EXPERIENCE_PROSE_MARKERS = { "worked", } +EXPERIENCE_PROSE_COMPANY_PATTERNS = ( + re.compile(r"\bau\s+sein\s+de\b", re.IGNORECASE), + re.compile(r"\b(before|after|during|while|since|because)\b", re.IGNORECASE), + re.compile(r"\b(joined|joining|moved|moving|relocated|relocating|worked|working)\b", re.IGNORECASE), + re.compile( + r"\bin\s+(?:paris|london|lyon|france|berlin|amsterdam|madrid|rome|marseille|bordeaux|toulouse|nantes|lille|grenoble|strasbourg|nice|rennes|montpellier|remote)\b", + re.IGNORECASE, + ), +) + EDUCATION_TITLE_KEYWORDS = { "bachelor", "degree", @@ -293,9 +318,6 @@ def _looks_like_experience_title(title: str) -> bool: return False first_word = title_words[0] - if len(first_word) < 2: - return False - normalized_title = " ".join(title_words).casefold() if normalized_title in EXPERIENCE_TITLE_LABELS: return False @@ -303,16 +325,16 @@ def _looks_like_experience_title(title: str) -> bool: if first_word.casefold() in EXPERIENCE_TITLE_STOPWORDS: return False + if first_word.casefold() in EXPERIENCE_TITLE_ACTION_STARTS: + return False + + if len(title_words) == 1 and len(first_word) < 2: + return False + return not any( word.casefold() in EXPERIENCE_PROSE_MARKERS for word in title_words if word ) def _looks_like_prose_company(company: str) -> bool: - company_words = [word.strip(".,;:!?()[]{}") for word in company.split()] - return any( - word - and word.lower() in EXPERIENCE_PROSE_MARKERS - and not (word.isupper() and len(word) <= 3) - for word in company_words - ) + return any(pattern.search(company) for pattern in EXPERIENCE_PROSE_COMPANY_PATTERNS) diff --git a/tests/profile/test_cv_extractor.py b/tests/profile/test_cv_extractor.py index 5a3b0ea..ede5758 100644 --- a/tests/profile/test_cv_extractor.py +++ b/tests/profile/test_cv_extractor.py @@ -169,7 +169,7 @@ def test_extract_cv_signals_parses_clear_titles_with_french_and_english_connecto ] -def test_extract_cv_signals_accepts_lowercase_mixed_case_and_numeric_titles() -> None: +def test_extract_cv_signals_accepts_lowercase_short_and_real_company_titles() -> None: text = dedent( """ Tonio @@ -177,6 +177,8 @@ def test_extract_cv_signals_accepts_lowercase_mixed_case_and_numeric_titles() -> data engineer at Company A iOS Engineer at Company A 3D Artist at Studio + R Developer at Company A + Data Engineer at Made in Design """ ).strip() @@ -186,6 +188,8 @@ def test_extract_cv_signals_accepts_lowercase_mixed_case_and_numeric_titles() -> {"title": "data engineer", "company": "Company A"}, {"title": "iOS Engineer", "company": "Company A"}, {"title": "3D Artist", "company": "Studio"}, + {"title": "R Developer", "company": "Company A"}, + {"title": "Data Engineer", "company": "Made in Design"}, ] @@ -219,6 +223,21 @@ def test_extract_cv_signals_rejects_label_like_lines_without_colons() -> None: assert extracted["experience_entries"] == [] +def test_extract_cv_signals_rejects_narrative_bullet_experience_lines() -> None: + text = dedent( + """ + Tonio + Location: France + Implemented data pipelines at Airbnb + Designed dashboards at Company A + """ + ).strip() + + extracted = extract_cv_signals(text) + + assert extracted["experience_entries"] == [] + + def test_extract_cv_signals_ignores_in_paris_prose_tail() -> None: text = dedent( """