""" 90% feasibility gate + status system (spec items #2-gate, #3-status). Before generating, estimate whether the candidate can realistically reach a 90%+ JD match AFTER aggressive plausible expansion — and decide how to proceed. Status lifecycle for any generated resume: READY_90_PLUS — JD match >= 90 and ATS readability >= 90 READY_90_PLUS_REVIEW_RECOMMENDED — reached 90 but used risky/review terms NEEDS_REPAIR — below 90, still in the repair loop NEEDS_USER_INPUT — below 90; needs user-confirmed credentials/terms NOT_ELIGIBLE_LOW_FIT — JD clearly unrelated to candidate background PARSE_FAILED — exported file failed parse validation """ from __future__ import annotations from dataclasses import dataclass, field from typing import List from .jd_analyzer import analyze_jd from .candidate_fit import ( classify_all_fit, includable, review_terms, ask_user_terms, blocked_terms, ) from .ats_scoring_v2 import score_jd_match, score_ats_readability # Status constants READY = "READY_90_PLUS" READY_REVIEW = "READY_90_PLUS_REVIEW_RECOMMENDED" # External-feedback repair only: internal + independent >= 90 AND the pasted # external-checker gaps were mostly resolved (no fabrication). READY_95_EXTERNAL_ALIGNED = "READY_95_EXTERNAL_ALIGNED" NEEDS_REPAIR = "NEEDS_REPAIR" NEEDS_USER_INPUT = "NEEDS_USER_INPUT" LOW_FIT = "NOT_ELIGIBLE_LOW_FIT" PARSE_FAILED = "PARSE_FAILED" # ── Maximum ATS Mode statuses (User-Confirmed Skill Expansion) ─────────────── # READY_MAX_ATS_95_PLUS — internal + independent + readability pass AND # external-style coverage >= 95% (or pasted external score >= 95). READY_MAX_ATS_95_PLUS = "READY_MAX_ATS_95_PLUS" # READY_90_PLUS_EXTERNAL_ALIGNED — the same gates pass AND coverage >= 90%. READY_90_PLUS_EXTERNAL_ALIGNED = "READY_90_PLUS_EXTERNAL_ALIGNED" # NEEDS_USER_CONFIRMATION — remaining gaps are HIGH-risk-but-supportable terms # the user can confirm with one click (then we regenerate). NEEDS_USER_CONFIRMATION = "NEEDS_USER_CONFIRMATION" # BELOW_TARGET_REPAIRABLE — below target but remaining gaps are LOW/MEDIUM, so # the system should keep repairing rather than stop. BELOW_TARGET_REPAIRABLE = "BELOW_TARGET_REPAIRABLE" # Statuses that represent a downloadable, target-meeting result. MAX_ATS_READY_STATUSES = frozenset({ READY, READY_REVIEW, READY_95_EXTERNAL_ALIGNED, READY_MAX_ATS_95_PLUS, READY_90_PLUS_EXTERNAL_ALIGNED, }) @dataclass class FitAssessment: eligible_for_auto_resume: bool estimated_max_score: int estimated_max_with_review: int explicit_matches: List[str] = field(default_factory=list) plausible_matches: List[str] = field(default_factory=list) adjacent_matches: List[str] = field(default_factory=list) risky_matches: List[str] = field(default_factory=list) blocked_matches: List[str] = field(default_factory=list) recommendation: str = "generate" # generate|generate_with_review|ask_user|skip def to_dict(self): return self.__dict__ def _synthetic_text(base_text: str, terms: List[str]) -> str: """A best-case resume text = base + the terms we'd include, for estimating the achievable JD-match ceiling.""" return base_text + "\n" + " . ".join(terms) def assess_job_fit_for_90(jd_text: str, base_resume_text: str, llm=None, cfg: dict = None, mode: str = "aggressive_plausible_match") -> FitAssessment: req = analyze_jd(jd_text, llm=llm, cfg=cfg) verdicts = classify_all_fit(req, base_resume_text) inc = [v.keyword for v in includable(verdicts)] rev = [v.keyword for v in review_terms(verdicts)] ask = [v.keyword for v in ask_user_terms(verdicts)] blk = [v.keyword for v in blocked_terms(verdicts)] by_status = lambda s: [v.keyword for v in verdicts if v.fit_status == s] # Estimate ceilings: score a synthetic resume containing the include set, # and another that also adds the risky/review set. est_inc = score_jd_match(_synthetic_text(base_resume_text, inc), req).score est_rev = score_jd_match(_synthetic_text(base_resume_text, inc + rev), req).score # Recommendation if est_inc >= 90: rec, eligible = "generate", True elif est_rev >= 90: rec, eligible = "generate_with_review", True elif ask and est_rev >= 80: rec, eligible = "ask_user", False elif est_rev >= 70: # Worth generating, will likely land in NEEDS_REPAIR/REVIEW rec, eligible = "generate_with_review", True else: rec, eligible = "skip", False return FitAssessment( eligible_for_auto_resume=eligible, estimated_max_score=est_inc, estimated_max_with_review=est_rev, explicit_matches=by_status("explicit"), plausible_matches=by_status("plausible"), adjacent_matches=by_status("adjacent"), risky_matches=rev, blocked_matches=blk, recommendation=rec, )