Spaces:
Sleeping
Sleeping
| """ | |
| Candidate Fit Expansion (spec: aggressive_plausible_match mode). | |
| Philosophy shift: the uploaded resume is a BASE PROFILE, not the candidate's | |
| full experience record. "Not in the resume" != "false". For a strong ATS resume | |
| we INCLUDE every JD requirement that is plausible for the candidate's role, | |
| seniority, and domain β the candidate validates in the interview. | |
| We still refuse to fabricate the things that are checkable and damaging to lie | |
| about: regulated certifications/licenses, degrees not present, unrelated deep | |
| technical specialties, and seniority jumps. Fake employers/dates/achievements | |
| are handled elsewhere (the tailoring contract never invents those). | |
| Per-requirement output: | |
| fit_status β explicit | plausible | adjacent | risky | blocked | |
| action β include | include_carefully | ask_user | block | |
| """ | |
| from __future__ import annotations | |
| import re | |
| from dataclasses import dataclass, field, asdict | |
| from typing import List, Dict | |
| from .ats_scorer import _kw_in_text | |
| from .jd_analyzer import Requirement, JDRequirements | |
| from .evidence_matcher import _RELATED, _PM_CORE_TRANSFERABLE | |
| # ββ BLOCK rules β the only hard "no". Everything else is includable. ββββββββββ | |
| # Regulated / credential terms: claiming these without holding them is harmful | |
| # and checkable. Surfaced as ask_user (degrees) or block (licenses/clearances). | |
| _REGULATED_CRED = re.compile( | |
| r"\b(cpa|cfa|series\s?\d+|bar exam|licensed|license\b|pmp\b|csm\b|cissp|" | |
| r"ccsp|cisa|cism|comptia|security\+|aws certified|gcp certified|" | |
| r"azure certified|certified scrum|six sigma (black|green) belt|" | |
| r"md\b|m\.?d\.?|rn\b|registered nurse|security clearance|clearance\b|" | |
| r"professional engineer|pe license)\b", re.I) | |
| # Deep technical specialties that don't fit a Product Manager profile β claiming | |
| # hands-on here is an unrealistic specialty jump. (Tool-level familiarity may be | |
| # fine; hands-on engineering is risky/blocked.) | |
| _DEEP_TECH_SPECIALTY = { | |
| "kernel", "firmware", "embedded systems", "fpga", "vlsi", "rtl", "verilog", | |
| "assembly", "device drivers", "bioinformatics", "actuarial", "soldering", | |
| "circuit design", "cryptography research", "penetration testing", | |
| "reverse engineering", "malware analysis", "exploit development", | |
| } | |
| # Hands-on software-engineering hard skills. A PM may COORDINATE these but | |
| # claiming them as personal hard skills on a PM resume is a specialty mismatch β | |
| # treat as RISKY (review), and BLOCK the most clearly engineer-only ones. This | |
| # is what keeps a backend-engineer JD from becoming a CLEAN_90 PM resume. | |
| _ENGINEERING_HARDCORE = { | |
| "java", "c++", "c#", "golang", "go programming", "rust", "scala", "kotlin", | |
| "spring boot", "spring framework", ".net", "microservices architecture", | |
| "distributed systems", "system design", "data structures", "algorithms", | |
| "multithreading", "concurrency", "jvm", "garbage collection", "compilers", | |
| "operating systems", "computer networks", "tcp/ip", "grpc", "graphql api design", | |
| "database internals", "query optimization", "kafka", "rabbitmq", "redis", | |
| "elasticsearch internals", "hadoop", "spark", "mapreduce", "cuda", | |
| "tensorflow", "pytorch", "deep learning models", "model training", | |
| "backend development", "frontend development", "full stack development", | |
| "react.js", "angular", "node.js", "android development", "ios development", | |
| } | |
| # Seniority signals above the candidate's level (set after inferring level). | |
| _SENIOR_TERMS = {"vp", "vice president", "director", "head of", "chief", | |
| "cto", "cpo", "ceo", "svp", "evp", "20+ years", "15+ years"} | |
| # Specialized DOMAIN tooling/knowledge β includable but flagged for review when | |
| # it's outside the candidate's evidenced domains (use_carefully). | |
| _SPECIALIZED_DOMAIN = { | |
| "siem", "soar", "xdr", "edr", "threat intelligence", "threat detection", | |
| "security operations", "secops", "incident response", "vulnerability management", | |
| "hl7", "fhir", "hipaa", "clinical", "pharmacovigilance", "underwriting", | |
| "actuarial", "basel", "kyc", "aml", "pci dss", "swift", "fix protocol", | |
| } | |
| # Maximum ATS Mode: normal PM/Product/AI/SaaS/B2B/agile vocabulary treated as | |
| # user-confirmed / interview-supportable (loaded from config; safe fallback). | |
| try: | |
| from config import MAXIMUM_ATS_SAFE_TERMS as _MAX_ATS_SAFE | |
| except Exception: # pragma: no cover - config import safety | |
| _MAX_ATS_SAFE = set() | |
| def _is_max_ats_safe(term: str) -> bool: | |
| """True when `term` is normal target-role PM/AI/SaaS vocabulary that | |
| Maximum ATS Mode may treat as user-confirmed. Never matches credentials, | |
| seniority, employers, or specialized hands-on engineering terms.""" | |
| return term.lower().strip() in _MAX_ATS_SAFE | |
| class FitVerdict: | |
| keyword: str | |
| category: str | |
| fit_status: str # explicit|plausible|adjacent|risky|blocked | |
| action: str # include|include_carefully|ask_user|block | |
| reason: str = "" | |
| importance: str = "preferred" | |
| recommended_placement: List[str] = field(default_factory=list) | |
| def to_dict(self): | |
| return asdict(self) | |
| def infer_seniority(base_resume_text: str) -> str: | |
| """Rough candidate level from the base resume.""" | |
| low = base_resume_text.lower() | |
| if any(t in low for t in ("vp ", "vice president", "director", "head of", "chief")): | |
| return "senior_plus" | |
| m = re.search(r"(\d{1,2})\+?\s*years", low) | |
| yrs = int(m.group(1)) if m else 0 | |
| if yrs >= 8 or "senior" in low or "lead" in low or "principal" in low: | |
| return "senior" | |
| return "mid" | |
| def _candidate_years(base_resume_text: str) -> int: | |
| yrs = [int(m) for m in re.findall(r"(\d{1,2})\+?\s*years?", base_resume_text.lower())] | |
| return max(yrs) if yrs else 0 | |
| def _is_blocked(term: str, category: str, seniority: str, | |
| candidate_years: int = 0) -> tuple: | |
| t = term.lower().strip() | |
| # Seniority overreach: a JD year requirement above the candidate's actual | |
| # experience must NOT be claimed (would be a fabricated tenure). | |
| if category == "seniority": | |
| m = re.search(r"(\d{1,2})", t) | |
| if m and candidate_years and int(m.group(1)) > candidate_years + 1: | |
| return ("block", f"requires {m.group(1)}y; candidate has ~{candidate_years}y") | |
| if _REGULATED_CRED.search(t): | |
| # Degrees/credentials β ask the user rather than hard-block (they may hold it) | |
| if category in ("certification", "education"): | |
| return ("ask_user", "credential β confirm you hold it") | |
| return ("block", "regulated credential/license not in profile") | |
| if t in _DEEP_TECH_SPECIALTY: | |
| return ("block", "deep technical specialty outside a PM profile") | |
| if t in _ENGINEERING_HARDCORE: | |
| # Engineer-only hard skill on a PM resume β review, don't auto-include. | |
| return ("ask_user", "hands-on engineering skill β confirm before claiming") | |
| if t in _SENIOR_TERMS and seniority != "senior_plus": | |
| return ("block", "seniority above candidate level") | |
| return ("", "") | |
| def classify_fit(req: Requirement, base_resume_text: str, | |
| candidate_domains: set = None, seniority: str = "mid", | |
| confirmed: set = None, blocked: set = None, | |
| candidate_years: int = 0, | |
| maximum_ats_mode: bool = False) -> FitVerdict: | |
| term = req.term | |
| tl = term.lower().strip() | |
| low = base_resume_text.lower() | |
| # 0a. Vault overrides (strongest signal β the user's own decisions) | |
| if blocked and tl in blocked: | |
| return FitVerdict(term, req.category, "blocked", "block", | |
| "user-marked do-not-use", req.importance, | |
| req.recommended_placement) | |
| if confirmed and tl in confirmed: | |
| return FitVerdict(term, req.category, "explicit", "include", | |
| "user-confirmed", req.importance, | |
| req.recommended_placement) | |
| # 0. Hard blocks first β Maximum ATS Mode does NOT relax these (degrees, | |
| # certs/licenses, regulated credentials, seniority jumps, deep specialties, | |
| # hands-on engineering). The honesty boundary is absolute. | |
| act, why = _is_blocked(term, req.category, seniority, candidate_years) | |
| if act == "block": | |
| return FitVerdict(term, req.category, "blocked", "block", why, | |
| req.importance, req.recommended_placement) | |
| if act == "ask_user": | |
| return FitVerdict(term, req.category, "risky", "ask_user", why, | |
| req.importance, req.recommended_placement) | |
| # 0b. Maximum ATS Mode: normal PM/Product/AI/SaaS/B2B/agile vocabulary is | |
| # treated as user-confirmed (the base resume is incomplete; these are | |
| # interview-supportable craft terms). Runs AFTER hard blocks so credentials/ | |
| # seniority/engineering still win, and only for the curated safe set. | |
| if maximum_ats_mode and _is_max_ats_safe(term): | |
| return FitVerdict(term, req.category, "explicit", "include", | |
| "Maximum ATS Mode: user-confirmed PM/AI craft term", | |
| req.importance, req.recommended_placement) | |
| # 1. Explicit β already in the resume | |
| if _kw_in_text(term, low) or any(_kw_in_text(a, low) for a in (req.aliases or [])): | |
| return FitVerdict(term, req.category, "explicit", "include", | |
| "present in resume", req.importance, req.recommended_placement) | |
| # 2. Adjacent β related evidence present in the resume | |
| related = _RELATED.get(tl, []) | |
| if any(_kw_in_text(r, low) for r in related): | |
| return FitVerdict(term, req.category, "adjacent", "include", | |
| "related experience present", req.importance, | |
| req.recommended_placement) | |
| # 3. Specialized domain knowledge outside evidenced domains β risky (include | |
| # carefully, flag for review). NOT in the candidate's domain β review. | |
| if tl in _SPECIALIZED_DOMAIN: | |
| in_domain = candidate_domains and tl in candidate_domains | |
| if in_domain: | |
| return FitVerdict(term, req.category, "adjacent", "include", | |
| "within candidate domain", req.importance, | |
| req.recommended_placement) | |
| return FitVerdict(term, req.category, "risky", "include_carefully", | |
| "specialized domain term β review before applying", | |
| req.importance, req.recommended_placement) | |
| # 4. PM-universal craft / soft skills / standard tools+methods β PLAUSIBLE. | |
| # The aggressive-expansion default: reasonable for the candidate's role. | |
| if (req.category in ("hard_skill", "tool", "responsibility", "soft_skill", "domain") | |
| or tl in _PM_CORE_TRANSFERABLE): | |
| return FitVerdict(term, req.category, "plausible", "include", | |
| "plausible for candidate role/seniority", req.importance, | |
| req.recommended_placement) | |
| # 5. Seniority signals / titles that match level β plausible; else handled above | |
| if req.category in ("seniority",): | |
| return FitVerdict(term, req.category, "plausible", "include", | |
| "matches candidate level", req.importance, | |
| req.recommended_placement) | |
| # Fallback: treat as plausible (aggressive mode) unless it was blocked above. | |
| return FitVerdict(term, req.category, "plausible", "include", | |
| "default-include (aggressive plausible match)", | |
| req.importance, req.recommended_placement) | |
| def classify_all_fit(req: JDRequirements, base_resume_text: str, | |
| candidate_domains: set = None, | |
| use_vault: bool = True, | |
| maximum_ats_mode: bool = False, | |
| extra_confirmed: set = None) -> List[FitVerdict]: | |
| seniority = infer_seniority(base_resume_text) | |
| cand_years = _candidate_years(base_resume_text) | |
| confirmed = blocked = None | |
| if use_vault: | |
| try: | |
| from .candidate_vault import user_confirmed_terms, user_blocked_terms | |
| confirmed = user_confirmed_terms() | |
| blocked = user_blocked_terms() | |
| except Exception: | |
| pass | |
| # Per-request user confirmations (e.g. extension "confirm these terms") merge | |
| # on top of the persisted vault, but never override a vault block. | |
| if extra_confirmed: | |
| confirmed = set(confirmed or set()) | {t.lower().strip() for t in extra_confirmed} | |
| if blocked: | |
| confirmed -= blocked | |
| return [classify_fit(r, base_resume_text, candidate_domains, seniority, | |
| confirmed=confirmed, blocked=blocked, | |
| candidate_years=cand_years, | |
| maximum_ats_mode=maximum_ats_mode) | |
| for r in req.all_requirements()] | |
| # ββ Risk severity (AUTO_AGGRESSIVE mode) βββββββββββββββββββββββββββββββββββββ | |
| LOW = "LOW_RISK_AUTO_INCLUDED" | |
| MEDIUM = "MEDIUM_RISK_REVIEW_RECOMMENDED" | |
| HIGH = "HIGH_RISK_NEEDS_CONFIRMATION" | |
| BLOCKED = "BLOCKED_DO_NOT_INCLUDE" | |
| def severity(v: FitVerdict) -> str: | |
| """Map a fit verdict to a risk severity level. | |
| LOW β common PM/product/business/analytics/agile terms, normal tools, | |
| normal responsibilities, soft skills, anything already in the resume. | |
| MEDIUM β domain/industry terms & plausible tools not in the base resume | |
| (auto-include but flag for review). | |
| HIGH β specialized platforms (SIEM/SOAR), compliance/regulatory terms, | |
| engineering hard skills, seniority-sensitive, credential-required | |
| (only used with explicit user confirmation). | |
| BLOCKEDβ degrees/certs/licenses/fakes/seniority jumps/deep specialties. | |
| """ | |
| if v.action == "block": | |
| return BLOCKED | |
| if v.action in ("ask_user", "include_carefully"): | |
| # specialized-domain / engineering / credential / regulatory β confirm | |
| return HIGH | |
| # action == include | |
| if v.fit_status == "explicit": | |
| return LOW | |
| if v.category == "domain": | |
| return MEDIUM | |
| if v.fit_status == "adjacent": | |
| return MEDIUM | |
| return LOW # plausible common PM craft / tools / methods / soft skills | |
| def by_severity(verdicts: List[FitVerdict], level: str) -> List[FitVerdict]: | |
| return [v for v in verdicts if severity(v) == level] | |
| # ββ Convenience splits for the tailoring engine ββββββββββββββββββββββββββββββ | |
| def includable(verdicts: List[FitVerdict]) -> List[FitVerdict]: | |
| """explicit + plausible + adjacent β safe to include in the resume.""" | |
| return [v for v in verdicts if v.action == "include"] | |
| def auto_terms(verdicts: List[FitVerdict]) -> List[FitVerdict]: | |
| """AUTO_AGGRESSIVE: LOW + MEDIUM are auto-included (MEDIUM flags review).""" | |
| return [v for v in verdicts if severity(v) in (LOW, MEDIUM)] | |
| def high_risk_terms(verdicts: List[FitVerdict]) -> List[FitVerdict]: | |
| """HIGH β only included with user confirmation; gate the job if needed for 90.""" | |
| return [v for v in verdicts if severity(v) == HIGH] | |
| def review_terms(verdicts: List[FitVerdict]) -> List[FitVerdict]: | |
| """risky β include only when needed for 90%, flagged for user review.""" | |
| return [v for v in verdicts if v.action == "include_carefully"] | |
| def ask_user_terms(verdicts: List[FitVerdict]) -> List[FitVerdict]: | |
| return [v for v in verdicts if v.action == "ask_user"] | |
| def blocked_terms(verdicts: List[FitVerdict]) -> List[FitVerdict]: | |
| return [v for v in verdicts if v.action == "block"] | |