""" External-ATS coverage simulator (Jobalytics/Simplify-style breadth). WHY THIS EXISTS ─────────────── Our internal scorer extracts keywords from a NARROW PM taxonomy and scores the resume against that same narrow set → it easily reports 90%+. External checkers (Jobalytics/Simplify) extract a MUCH broader set (40-46 terms incl. domain words, JD-specific responsibilities, soft skills) → the same resume scores ~54%. The internal number is therefore NOT a valid success signal for the external goal. This module computes a broad, Jobalytics-style EXPECTED keyword set from the JD, and measures coverage against the *re-parsed exported resume text* (the only truth). Used to (a) decide what to physically place in Maximum ATS Mode and (b) gate/report readiness on real external-style coverage, not internal score. It is intentionally broader than `extract_jd_keywords` (which stays skills-only to keep the honest default pipeline clean). The broad set is only used in Maximum ATS Mode / external-feedback repair, where the user has explicitly opted into aggressive coverage. """ from __future__ import annotations import re from typing import Dict, List from .ats_scorer import _kw_in_text, extract_jd_keywords # Words that are never standalone keywords (checkers penalise/ignore them). _STOP = { "the", "and", "for", "with", "you", "your", "our", "are", "will", "have", "has", "had", "this", "that", "these", "those", "from", "into", "out", "off", "who", "what", "when", "where", "why", "how", "all", "any", "can", "may", "should", "would", "could", "must", "able", "etc", "per", "via", "not", "but", "they", "them", "their", "his", "her", "its", "she", "him", "was", "were", "been", "being", "more", "most", "such", "than", "then", "also", "about", "across", "within", "while", "each", "other", "some", "many", "well", "very", "much", "like", "just", "only", "even", "both", "over", "under", "between", "during", "including", "include", "includes", "looking", "join", "join us", "work", "working", "team", "teams", "role", "job", "company", "candidate", "candidates", "ideal", "great", "good", "strong", "years", "year", "experience", "experiences", "plus", "preferred", "required", "requirement", "responsibilities", "qualifications", "skills", "ability", "knowledge", "understanding", "familiarity", "proficiency", "we", "us", "is", "in", "on", "of", "to", "at", "as", "an", "or", "be", "it", "by", "a", "i", } # Vague filler — never a keyword (mirrors jd_analyzer._BUZZWORDS intent). _FILLER = { "innovation", "innovative", "solution", "solutions", "tools", "tool", "ownership", "synergy", "dynamic", "passionate", "motivated", "self-starter", "results-driven", "detail-oriented", "team player", "track record", "expertise", "best practices", "thought leadership", "fast-paced", "cutting-edge", "world-class", "robust", "seamless", "holistic", "leverage", "excellence", "proven", "successful", "goals", "productivity", "reinvent", "reinvention", "mission", "vision", "culture", "value", "values", "impact", "environment", "opportunity", "opportunities", "responsibility", } # UI/CTA/marketing phrases injected by checker overlays (Simplify/Jobalytics/…) # — never genuine JD keywords. Exact lowercased matches. _UI_NOISE = frozenset({ "show match details", "people clicked apply", "month free trial", "free trial", "actively reviewing applicants", "message hiring managers", "get personalized cover letter", "get insider access", "members use premium", "recruiting bond", "visit website", "lacs pa", "help me stand", "tailor my resume", "uses advanced ai", "try premium", "easy apply", "see how you compare", "am i a good fit", "save job", "apply now", "show more", "show less", "sign in", "join now", }) # CTA/marketing lead tokens: a multi-word gram starting with one is UI noise. _UI_LEAD = { "show", "get", "try", "visit", "apply", "save", "click", "join", "sign", "message", "see", "tailor", "unlock", "upgrade", "start", } # Marketing substrings that mark a gram as UI noise wherever they appear. _UI_SUBSTR = ( "free trial", "premium", "hiring manager", "cover letter", "clicked apply", "match details", "lacs pa", ) def _clean(term: str) -> str: return re.sub(r"[^\w\s/+.\-]", "", term or "").strip().lower() def _is_ui_noise(t: str) -> bool: """True if `t` is a checker-overlay CTA / marketing phrase, not a JD term.""" t = (t or "").strip() if not t: return False if t in _UI_NOISE: return True words = t.split() if len(words) > 1 and words[0] in _UI_LEAD: return True if any(sub in t for sub in _UI_SUBSTR): return True return False def _is_term_like(t: str) -> bool: """A token/phrase that reads like a real skill/responsibility/domain term.""" t = t.strip() if not t or t in _STOP or t in _FILLER: return False if _is_ui_noise(t): return False words = t.split() if len(words) > 4: return False if len(t) < 3: return False if t.isdigit(): return False # All words must be non-stop and not lemmatizer artifacts. for w in words: if w in _STOP: return False if len(t) >= 5 and t.endswith(("at", "iz", "ic")) and len(words) == 1: return False # "integrat", "automat", "operat" return True def extract_external_keywords(jd_text: str, extra: List[str] = None) -> List[str]: """Broad, Jobalytics-style expected keyword set for a JD. Union of: the reliable taxonomy floor (`extract_jd_keywords`), curated Maximum-ATS safe terms present in the JD, and term-like 1-3 word phrases pulled from the JD body (filtered against stopwords/filler). De-duplicated, lowercase. `extra` (e.g. pasted external missing/matched terms) is unioned in. """ jd_text = jd_text or "" jd_low = jd_text.lower() out: List[str] = [] seen = set() def _add(term: str): t = _clean(term) if t and t not in seen and _is_term_like(t): seen.add(t) out.append(t) # 1. Reliable taxonomy floor. for k in extract_jd_keywords(jd_text): _add(k) # 2. Curated safe vocabulary that actually appears in this JD. try: from config import MAXIMUM_ATS_SAFE_TERMS as _SAFE except Exception: _SAFE = set() for t in _SAFE: if t in jd_low: _add(t) # 3. Term-like phrases from the JD body. Pull capitalised/section phrases and # notable bigrams/trigrams. Conservative: only multiword phrases whose # words are all alphabetic and non-stop, plus known-good single nouns. # (Single-noun garbage is filtered by _is_term_like + the dedup below.) # Bi/tri-grams of alphabetic words. words = re.findall(r"[a-zA-Z][a-zA-Z\-/+.]{1,}", jd_low) for n in (3, 2): for i in range(len(words) - n + 1): gram = " ".join(words[i:i + n]) if all(w not in _STOP and w not in _FILLER for w in words[i:i + n]): # Only keep grams that recur or look like a skill phrase. if jd_low.count(gram) >= 1 and len(gram) <= 34: # Skip grams that are mostly filler-ish single words joined. _add(gram) # 4. Pasted external terms (ground truth from a checker). for t in (extra or []): _add(t) return out def external_coverage(expected: List[str], exported_text: str) -> Dict: """Measure coverage of `expected` terms against the exported resume text. Returns {expected, found, pct, present, missing}. Matching uses the same word-boundary/phrase logic as scoring (`_kw_in_text`). """ text = (exported_text or "").lower() exp = [] seen = set() for t in expected: tl = _clean(t) if tl and tl not in seen: seen.add(tl) exp.append(tl) present = [t for t in exp if _kw_in_text(t, text)] missing = [t for t in exp if t not in present] total = len(exp) return { "expected": total, "found": len(present), "pct": int(round(100 * len(present) / max(1, total))), "present": present, "missing": missing, }