JAA-ATS-Tool / src /fit_gate.py
saitejatirunagari's picture
feat(ats): Maximum ATS Mode (User-Confirmed Skill Expansion) + coverage report
b3704a6
Raw
History Blame
5.07 kB
"""
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,
)