Spaces:
Sleeping
Sleeping
| """ | |
| Candidate Experience Expansion Vault (spec item #6). | |
| A persistent store of everything we've learned about the candidate's usable | |
| experience across jobs — so the system gets stronger over time and stays | |
| consistent (a term confirmed safe on one job stays safe on the next). | |
| Persisted to data/candidate_vault.json. Each entry: | |
| { term, category, source, confidence, usage_guidance, example_bullet } | |
| source ∈ resume | inferred_plausible | user_confirmed | jd_expansion | |
| confidence ∈ high | medium | low | |
| usage_guidance ∈ safe_to_use | use_carefully | ask_user | blocked | |
| The vault FEEDS the fit classifier: | |
| - user_confirmed terms → always safe (treated as explicit) | |
| - blocked terms (user said no)→ always blocked | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| from typing import Dict, List | |
| _VAULT_PATH = "data/candidate_vault.json" | |
| # Per spec #8: only resume_original + user_confirmed are FULLY safe. Everything | |
| # inferred/expanded stays REVIEWABLE — the vault must never silently promote a | |
| # risky/plausible JD term to "safe" just because it appeared in past resumes. | |
| _FIT_TO_GUIDANCE = { | |
| "explicit": "safe_to_use", # actually present in the original resume | |
| "plausible": "use_carefully", # reviewable until user confirms | |
| "adjacent": "use_carefully", | |
| "risky": "ask_user", | |
| "blocked": "blocked", | |
| } | |
| _FIT_TO_SOURCE = { | |
| "explicit": "resume_original", | |
| "plausible": "inferred_plausible", | |
| "adjacent": "inferred_plausible", | |
| "risky": "risky_review", | |
| "blocked": "blocked", | |
| } | |
| _FIT_TO_CONF = {"explicit": "high", "plausible": "medium", "adjacent": "medium", | |
| "risky": "low", "blocked": "low"} | |
| def load_vault(path: str = _VAULT_PATH) -> Dict[str, dict]: | |
| """Return {term_lower: entry}. Empty dict if none yet.""" | |
| try: | |
| with open(path, encoding="utf-8") as f: | |
| data = json.load(f) | |
| return {e["term"].lower(): e for e in data.get("entries", []) if e.get("term")} | |
| except Exception: | |
| return {} | |
| def save_vault(vault: Dict[str, dict], path: str = _VAULT_PATH) -> None: | |
| try: | |
| os.makedirs(os.path.dirname(path), exist_ok=True) | |
| with open(path, "w", encoding="utf-8") as f: | |
| json.dump({"entries": list(vault.values())}, f, ensure_ascii=False, indent=2) | |
| except Exception as e: | |
| print(f"[vault] save skipped: {e}") | |
| def update_from_fit(verdicts, path: str = _VAULT_PATH) -> Dict[str, dict]: | |
| """Merge a job's FitVerdicts into the vault. user_confirmed entries are never | |
| downgraded by automated runs.""" | |
| vault = load_vault(path) | |
| for v in verdicts: | |
| key = v.keyword.lower().strip() | |
| if not key: | |
| continue | |
| existing = vault.get(key) | |
| # Never overwrite a user_confirmed / user-blocked decision automatically. | |
| if existing and existing.get("source") == "user_confirmed": | |
| continue | |
| vault[key] = { | |
| "term": v.keyword, | |
| "category": v.category, | |
| "source": _FIT_TO_SOURCE.get(v.fit_status, "jd_expansion"), | |
| "confidence": _FIT_TO_CONF.get(v.fit_status, "low"), | |
| "usage_guidance": _FIT_TO_GUIDANCE.get(v.fit_status, "use_carefully"), | |
| "example_bullet": (existing or {}).get("example_bullet", ""), | |
| } | |
| save_vault(vault, path) | |
| return vault | |
| def confirm_terms(terms: List[str], confirmed: bool = True, | |
| path: str = _VAULT_PATH) -> None: | |
| """User explicitly confirms (or rejects) terms — the strongest signal.""" | |
| vault = load_vault(path) | |
| for t in terms: | |
| key = t.lower().strip() | |
| if not key: | |
| continue | |
| e = vault.get(key, {"term": t, "category": "user", "example_bullet": ""}) | |
| e["source"] = "user_confirmed" | |
| e["confidence"] = "high" | |
| e["usage_guidance"] = "safe_to_use" if confirmed else "blocked" | |
| vault[key] = e | |
| save_vault(vault, path) | |
| def user_confirmed_terms(path: str = _VAULT_PATH) -> set: | |
| v = load_vault(path) | |
| return {k for k, e in v.items() | |
| if e.get("source") == "user_confirmed" and e.get("usage_guidance") == "safe_to_use"} | |
| def user_blocked_terms(path: str = _VAULT_PATH) -> set: | |
| v = load_vault(path) | |
| return {k for k, e in v.items() if e.get("usage_guidance") == "blocked"} | |
| def confirm_expansion_terms(terms: List[str], category: str = "max_ats_expansion", | |
| path: str = _VAULT_PATH) -> List[str]: | |
| """Persist Maximum-ATS / user-confirmed expansion terms as user_confirmed so | |
| future resumes treat them as safe. Returns the list of newly-stored terms. | |
| This is the explicit-confirmation path: the user confirmed (via the extension | |
| "Maximum ATS Mode" / "Confirm these terms") that these are interview-supportable. | |
| Truly high-risk terms are still only stored when the user confirms them here — | |
| nothing is silently promoted by an automated run. | |
| """ | |
| terms = [t for t in (terms or []) if t and t.strip()] | |
| if not terms: | |
| return [] | |
| vault = load_vault(path) | |
| blocked = {k for k, e in vault.items() if e.get("usage_guidance") == "blocked"} | |
| stored: List[str] = [] | |
| for t in terms: | |
| key = t.lower().strip() | |
| if not key or key in blocked: | |
| continue # never override a user block | |
| e = vault.get(key, {"term": t, "example_bullet": ""}) | |
| e["term"] = e.get("term") or t | |
| e["category"] = e.get("category") or category | |
| e["source"] = "user_confirmed" | |
| e["confidence"] = "high" | |
| e["usage_guidance"] = "safe_to_use" | |
| vault[key] = e | |
| stored.append(t) | |
| save_vault(vault, path) | |
| return stored | |
| def vault_summary(path: str = _VAULT_PATH) -> dict: | |
| """Reporting helper: counts + the user-confirmed term list, so the UI can | |
| show what's been added to the vault.""" | |
| v = load_vault(path) | |
| by_source: Dict[str, int] = {} | |
| for e in v.values(): | |
| by_source[e.get("source", "unknown")] = by_source.get(e.get("source", "unknown"), 0) + 1 | |
| return { | |
| "total": len(v), | |
| "by_source": by_source, | |
| "user_confirmed": sorted(user_confirmed_terms(path)), | |
| "blocked": sorted(user_blocked_terms(path)), | |
| } | |