{"record":{"id":"f59d44961281a776","repo":"srbhr/Resume-Matcher","slug":"personalinfo-fields-changed-join-mismatches","errorCode":null,"errorMessage":"personalInfo fields changed: {', '.join(mismatches)}","messagePattern":"personalInfo fields changed: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"apps/backend/app/routers/resumes.py","lineNumber":553,"sourceCode":"    if improved_info is None:\n        raise ValueError(\"Improved resume missing personalInfo\")\n    if not isinstance(original_info, dict):\n        raise ValueError(\n            f\"Original personalInfo is not a dict: {type(original_info).__name__}\"\n        )\n    if not isinstance(improved_info, dict):\n        raise ValueError(\n            f\"Improved personalInfo is not a dict: {type(improved_info).__name__}\"\n        )\n    fields = set(original_info.keys()) | set(improved_info.keys())\n    mismatches = [\n        field\n        for field in sorted(fields)\n        if _normalize_personal_info_value(original_info.get(field))\n        != _normalize_personal_info_value(improved_info.get(field))\n    ]\n    if mismatches:\n        raise ValueError(f\"personalInfo fields changed: {', '.join(mismatches)}\")\n\n\nasync def _generate_auxiliary_messages(\n    improved_data: dict[str, Any],\n    job_content: str,\n    language: str,\n    enable_cover_letter: bool,\n    enable_outreach: bool,\n    enable_interview_prep: bool,\n) -> tuple[str | None, str | None, str | None, InterviewPrepData | None, list[str]]:\n    \"\"\"Generate cover letter, outreach, interview prep, and resume title.\n\n    Returns (cover_letter, outreach_message, title, interview_prep, warnings).\n    \"\"\"\n    cover_letter = None\n    outreach_message = None\n    title = None\n    interview_prep = None","sourceCodeStart":535,"sourceCodeEnd":571,"githubUrl":"https://github.com/srbhr/Resume-Matcher/blob/116f9cc3b00e1ac91734a6c2679bf41ea64a0edc/apps/backend/app/routers/resumes.py#L535-L571","documentation":"The core immutability guarantee of the improve/confirm flow: after validating shape, _validate_confirm_payload unions the key sets of original and improved personalInfo, normalizes each value, and raises this ValueError listing every field whose value changed. It guarantees the tailoring pipeline never alters identity data (name, email, phone, location, links) — only experience/skills content. Surfaced as HTTP 400.","triggerScenarios":"POST /resumes/improve/confirm where any personalInfo field differs between the stored original and the submitted improved data — normalization differences like whitespace, case, or dash/date variants are folded away, so this fires only on real value changes (different email, missing phone, altered LinkedIn URL).","commonSituations":"LLM rewrote the identity block despite safety nets; a client (or script) modified personalInfo between preview and confirm; diff application edited personalInfo paths; the preview hash is fine but the payload drifted.","solutions":["Copy the original personalInfo values into the improved payload before confirming — identity fields must be byte-identical after normalization","Re-run preview and submit its improved data unchanged","Check whether a custom diff allow-list or prompt change is letting the LLM edit personalInfo paths, and block that path"],"exampleFix":"// before: pipeline changed the email in improved_data\n\"personalInfo\": {\"name\": \"Jane Doe\", \"email\": \"jane@newmail.com\"}\n// after: identity block identical to the original resume\n\"personalInfo\": {\"name\": \"Jane Doe\", \"email\": \"jane@example.com\"}","handlingStrategy":"validation","validationCode":"// client: pre-flight parity check before confirm (mirrors the server's normalization)\nfunction personalInfoChanged(orig, improved) {\n  const norm = v => (v ?? '').toString().replace(/\\s+/g, ' ').trim().toLowerCase();\n  const keys = new Set([...Object.keys(orig), ...Object.keys(improved)]);\n  return [...keys].filter(k => norm(orig[k]) !== norm(improved[k]));\n}\nconst mismatches = personalInfoChanged(original.personalInfo, improvedData.personalInfo);\nif (mismatches.length) throw new Error(`identity fields edited: ${mismatches}`);","typeGuard":null,"tryCatchPattern":"try {\n  await api.post('/resumes/improve/confirm', body);\n} catch (e) {\n  if (e.response?.status === 400 && /personalInfo fields changed/.test(e.response?.data?.detail ?? '')) {\n    // restore original identity block and resubmit\n    body.improved_data.personalInfo = structuredClone(original.personalInfo);\n    await api.post('/resumes/improve/confirm', body);\n  }\n}","preventionTips":["Make the personalInfo block read-only in the tailoring UI","Lock/block the personalInfo path in any custom diff allow-list so the LLM cannot edit it","Always take improved_data from the preview response instead of re-entering it","Compare identity fields against the original before calling confirm"],"tags":["validation","personal-info","immutability","confirm-endpoint"],"backgroundTag":"personal-info-mutation-rejected","analyzedSha":"116f9cc3b00e1ac91734a6c2679bf41ea64a0edc","analyzedAt":"2026-08-28T22:51:40.999Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}