From 3c331ef6879d461d5c2c5da007ac072122c0573d Mon Sep 17 00:00:00 2001 From: Antoine Date: Thu, 28 May 2026 18:07:51 +0200 Subject: [PATCH] feat: add canonical profile merger with warnings --- src/job_research/profile/merge.py | 56 +++++++++++++++++++++++++++++++ tests/profile/test_merge.py | 28 ++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 src/job_research/profile/merge.py create mode 100644 tests/profile/test_merge.py diff --git a/src/job_research/profile/merge.py b/src/job_research/profile/merge.py new file mode 100644 index 0000000..8718e23 --- /dev/null +++ b/src/job_research/profile/merge.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from job_research.models import ( + CandidateProfileOutput, + EducationEntry, + ExperienceEntry, + WarningItem, +) +from job_research.profile.profile_parser import AuthoredProfile + + +def build_candidate_profile_output( + cv_signals: dict[str, object], authored: AuthoredProfile +) -> CandidateProfileOutput: + warnings: list[WarningItem] = [] + + if "years_of_experience" in cv_signals and any( + "experience" in note.lower() for note in authored.notes + ): + warnings.append( + WarningItem( + field="years_of_experience", + message=( + "CV-derived experience may differ from markdown interpretation. " + "Review manually." + ), + ) + ) + + merged_skills: list[str] = [] + for skill in [ + *(cv_signals.get("skills") or []), + *authored.strengths, + *authored.skills_to_emphasize, + ]: + if skill not in merged_skills: + merged_skills.append(skill) + + experience_entries = cv_signals.get("experience_entries") or [] + education_entries = cv_signals.get("education_entries") or [] + + return CandidateProfileOutput( + name=cv_signals.get("name"), + summary=authored.summary, + target_roles=authored.target_roles, + strengths=authored.strengths, + skills_to_emphasize=authored.skills_to_emphasize, + constraints=authored.constraints, + notes=authored.notes, + location=cv_signals.get("location"), + languages=cv_signals.get("languages") or [], + skills=merged_skills, + experience_entries=[ExperienceEntry.model_validate(item) for item in experience_entries], + education_entries=[EducationEntry.model_validate(item) for item in education_entries], + warnings=warnings, + ) diff --git a/tests/profile/test_merge.py b/tests/profile/test_merge.py new file mode 100644 index 0000000..0892a9a --- /dev/null +++ b/tests/profile/test_merge.py @@ -0,0 +1,28 @@ +from job_research.profile.merge import build_candidate_profile_output +from job_research.profile.profile_parser import AuthoredProfile + + +def test_build_candidate_profile_output_writes_warning_when_facts_conflict() -> None: + cv_signals = { + "name": "Tonio", + "location": "France", + "languages": ["French", "English"], + "skills": ["Python", "SQL"], + "experience_entries": [{"title": "Data Engineer", "company": "A"}], + "education_entries": [], + "years_of_experience": 2, + } + authored = AuthoredProfile( + summary="Junior data engineer focused on GCP.", + target_roles=["Data Engineer"], + strengths=["Python"], + skills_to_emphasize=["BigQuery", "GCP"], + constraints=["CDI only"], + notes=["Years of experience feels closer to 3."], + ) + + output = build_candidate_profile_output(cv_signals, authored) + + assert output.summary == "Junior data engineer focused on GCP." + assert output.constraints == ["CDI only"] + assert output.warnings[0].field == "years_of_experience"