""" Per-job validation package (spec #9). For every generated resume we can export a self-contained folder that bundles everything needed for MANUAL Jobalytics / Simplify verification and for auditing the model-independent pipeline: final resume DOCX + PDF parsed_resume.txt (the EXPORTED file re-parsed back to text) jd.txt (the job description we tailored against) manifest.json (scores, keyword lists, risk terms, provider info) README.md (human-readable summary) It never generates or scores anything — it only collects what the pipeline already produced on the job dict (`_v2_report`). This is what the user opens to confirm our internal numbers against a real external checker. """ from __future__ import annotations import os import re import json import shutil from datetime import datetime def _safe(s: str, n: int = 60) -> str: return re.sub(r'[\\/*?:"<>|]', "", str(s or ""))[:n].strip() or "item" def build_validation_package(job: dict, base_resume_text: str = None, out_root: str = "data/output/validation") -> str: """Write a validation package folder for one job and return its path.""" report = job.get("_v2_report", {}) or {} est = report.get("estimated_scores", {}) or {} date_str = datetime.now().strftime("%Y-%m-%d") folder = os.path.join( out_root, date_str, f"{_safe(job.get('company', 'Company'), 40)}_{_safe(job.get('title', 'Role'), 40)}", ) os.makedirs(folder, exist_ok=True) # 1. Copy the resume files docx_path = job.get("resume_path", "") pdf_path = job.get("resume_pdf_path", "") parsed_text = "" if docx_path and os.path.exists(docx_path): try: shutil.copy2(docx_path, os.path.join(folder, "resume" + os.path.splitext(docx_path)[1])) except Exception: pass try: from .resume_customizer import _read_docx_text parsed_text = _read_docx_text(docx_path) except Exception: parsed_text = "" if pdf_path and os.path.exists(pdf_path): try: shutil.copy2(pdf_path, os.path.join(folder, "resume.pdf")) except Exception: pass # 2. Parsed text + JD jd_text = job.get("description", "") or "" with open(os.path.join(folder, "parsed_resume.txt"), "w", encoding="utf-8") as f: f.write(parsed_text) with open(os.path.join(folder, "jd.txt"), "w", encoding="utf-8") as f: f.write(jd_text) # 3. Manifest — every field spec #9 asks for risky = report.get("risky_review_table", []) or [] medium_terms = report.get("review_terms_for_user_review", []) or [] high_terms = report.get("high_risk_terms_for_confirmation", []) or [] blocked_terms = [d.get("keyword") for d in report.get("unsupported_missing_keywords", []) or []] manifest = { "job": { "title": job.get("title", ""), "company": job.get("company", ""), "location": job.get("location", ""), "platform": job.get("platform", job.get("source", "")), "url": job.get("url", ""), }, "status": job.get("status", report.get("status", "")), "download_allowed": bool(report.get("download_allowed", job.get("download_allowed"))), "quality_flag": report.get("quality_flag", job.get("quality_flag", "")), "scores": { "internal_jd_match": est.get("jd_match", job.get("ats_score_after", 0)), "independent_jd_match": report.get("independent_jd_match", job.get("independent_jd_match", 0)), "ats_readability": est.get("ats_readability", 0), "combined_range": est.get("combined_range", ""), }, "provider": { "provider_used": report.get("provider_used", job.get("provider_used", "")), "provider_response_quality": report.get("provider_response_quality", ""), "provider_attempts": report.get("provider_attempts", []), }, "repair_attempts": report.get("repair_attempts", []), "keywords": { "jd_covered_terms": report.get("covered_terms", []), "jd_missing_terms": report.get("missing_terms", []), "keywords_added_evidenced": report.get("evidenced_terms", []), "keywords_added_skills_only": report.get("skills_only_terms", []), "medium_risk_terms": medium_terms, "high_risk_terms": high_terms, "blocked_terms": blocked_terms, }, "risky_review_table": risky, "files": { "docx": os.path.basename(docx_path) if docx_path else "", "pdf": "resume.pdf" if (pdf_path and os.path.exists(os.path.join(folder, "resume.pdf"))) else "", "parsed_resume": "parsed_resume.txt", "jd": "jd.txt", }, "generated_at": datetime.now().isoformat(timespec="seconds"), } with open(os.path.join(folder, "manifest.json"), "w", encoding="utf-8") as f: json.dump(manifest, f, ensure_ascii=False, indent=2) # 4. Human-readable summary s = manifest["scores"] lines = [ f"# Validation Package — {job.get('title','')} at {job.get('company','')}", "", f"- Status: **{manifest['status']}** | Download allowed: {manifest['download_allowed']}", f"- Provider used: **{manifest['provider']['provider_used']}** " f"(schema: {manifest['provider']['provider_response_quality']})", f"- Internal JD match: **{s['internal_jd_match']}**", f"- Independent JD match: **{s['independent_jd_match']}**", f"- ATS readability: **{s['ats_readability']}**", f"- Repair attempts: {len(manifest['repair_attempts'])}", "", "## How to verify", "1. Open `resume.pdf` (or the DOCX) and upload it to Jobalytics / Simplify.", "2. Paste `jd.txt` as the job description.", "3. Compare the external score to the independent score above.", "4. Review `manifest.json` -> keywords.medium_risk_terms / high_risk_terms / blocked_terms.", "", "## Provider attempts", ] for a in manifest["provider"]["provider_attempts"]: lines.append(f"- {a}") with open(os.path.join(folder, "README.md"), "w", encoding="utf-8") as f: f.write("\n".join(lines)) return folder def build_packages_for_jobs(jobs: list, base_resume_text: str = None, out_root: str = "data/output/validation") -> list: """Build packages for a list of jobs; returns list of folder paths.""" paths = [] for job in jobs: if not job.get("resume_path"): continue try: paths.append(build_validation_package(job, base_resume_text, out_root)) except Exception as e: print(f"[validation-package] {job.get('company','?')}: {e}") return paths