Spaces:
Running
Running
| """ | |
| 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, | |
| } | |