from __future__ import annotations from dataclasses import dataclass from typing import Callable, Dict, Any, List, Tuple import re @dataclass class Guideline: id: str description: str condition: Callable[[Dict[str, Any]], bool] validate: Callable[[str, Dict[str, Any]], Tuple[bool, str]] enforce: Callable[[str, Dict[str, Any]], str] class GuidelineEngine: def __init__(self, rules: List[Guideline]) -> None: self.rules = rules def check_and_enforce(self, text: str, ctx: Dict[str, Any]) -> Tuple[str, List[str], List[str]]: matched: List[str] = [] fixed: List[str] = [] out = text or "" for g in self.rules: try: if not g.condition(ctx): continue matched.append(g.id) ok, _ = g.validate(out, ctx) if not ok: out = g.enforce(out, ctx) fixed.append(g.id) except Exception: # fail-safe, do not block continue return out, matched, fixed # ---------- Helpers ---------- _BUZZWORDS = [ "results-driven", "team player", "people person", "perfectionist", "multi-tasker", "multi tasker", "dynamic go-getter", "rockstar", "guru", "ninja" ] _WEAK_OPENERS = [ (re.compile(r"^\s*[-•]\s*responsible for\s+", re.I), "- Led "), (re.compile(r"^\s*[-•]\s*tasked with\s+", re.I), "- Executed "), (re.compile(r"^\s*[-•]\s*worked on\s+", re.I), "- Delivered "), (re.compile(r"^\s*[-•]\s*helped\s+", re.I), "- Supported "), (re.compile(r"^\s*[-•]\s*assisted with\s+", re.I), "- Supported "), (re.compile(r"^\s*[-•]\s*handled\s+", re.I), "- Managed "), ] def _enforce_exact_length(text: str, target_len: int) -> str: if target_len <= 0: return text or "" txt = (text or "") if len(txt) == target_len: return txt if len(txt) > target_len: return txt[:target_len] return txt + (" " * (target_len - len(txt))) def _ensure_headings(text: str) -> str: """Ensure key headings exist: SUMMARY, SKILLS, EXPERIENCE, EDUCATION.""" t = text or "" low = t.lower() out = t def add_heading(h: str) -> None: nonlocal out if h.lower() not in low: out = (out + f"\n\n{h}\n").strip() for h in ["SUMMARY", "SKILLS", "EXPERIENCE", "EDUCATION"]: if h.lower() not in low: add_heading(h) return out def _strip_tabs(text: str) -> str: return (text or "").replace("\t", " ") def _scrub_buzzwords(text: str) -> str: out = text or "" low = out.lower() for bw in _BUZZWORDS: if bw in low: out = re.sub(re.escape(bw), "", out, flags=re.I) return out def _strengthen_action_verbs(text: str) -> str: lines = (text or "").splitlines() fixed: List[str] = [] for ln in lines: new_ln = ln for pat, repl in _WEAK_OPENERS: if pat.search(new_ln): new_ln = pat.sub(repl, new_ln) break fixed.append(new_ln) return "\n".join(fixed) def _remove_first_person(text: str) -> str: # Remove leading "I " / "My " in bullets only lines = (text or "").splitlines() out: List[str] = [] for ln in lines: m = re.match(r"^\s*[-•]\s*(i|my|we)\b", ln, flags=re.I) if m: ln = re.sub(r"^\s*([-•]\s*)(i|my|we)\b\s*", r"\1", ln, flags=re.I) out.append(ln) return "\n".join(out) def _ats_plain_text(text: str) -> str: # normalize bullets and strip odd symbols out = _strip_tabs(text) out = out.replace("•\t", "- ").replace("• ", "- ") out = re.sub(r"[■▪◦●○✔✦♦]", "-", out) return out def _enforce_uk_habits(text: str) -> str: # normalize currency symbol spacing and percentages out = re.sub(r"\s*£\s*", " £", text or "") out = re.sub(r"\s*%\s*", "%", out) return out def _allowed_skills_from_profile(ctx: Dict[str, Any]) -> List[str]: p = (ctx.get("profile_text") or "").lower() # naive split of alphanum skill-like tokens cands = re.findall(r"[a-zA-Z][a-zA-Z0-9+_.#-]{2,}", p) seen: Dict[str, int] = {} for c in cands: seen[c.lower()] = 1 return list(seen.keys()) def _no_invented_skills(text: str, ctx: Dict[str, Any]) -> Tuple[bool, str]: allowed = set(_allowed_skills_from_profile(ctx)) if not allowed: return True, "no baseline" skills_block = re.search(r"(?is)\n\s*(skills|core skills)[\s:]*\n(.+?)(\n\n|$)", text or "") if not skills_block: return True, "no skills block" block = skills_block.group(0) found = re.findall(r"[A-Za-z][A-Za-z0-9+_.#-]{2,}", block) for f in found: if f.lower() not in allowed: return False, f return True, "ok" # ---------- Rule sets ---------- def build_resume_rules() -> List[Guideline]: return [ Guideline( id="exact_length", description="Enforce exact target length when provided", condition=lambda ctx: bool(ctx.get("target_len")), validate=lambda txt, ctx: (len(txt or "") == int(ctx.get("target_len", 0)), "len"), enforce=lambda txt, ctx: _enforce_exact_length(txt, int(ctx.get("target_len", 0))), ), Guideline( id="headings_present", description="Ensure key headings exist", condition=lambda ctx: True, validate=lambda txt, ctx: (all(h.lower() in (txt or "").lower() for h in ["summary", "experience", "education", "skills"]), "headings"), enforce=lambda txt, ctx: _ensure_headings(txt), ), Guideline( id="ats_plain_text", description="Normalize bullets/tabs for ATS", condition=lambda ctx: True, validate=lambda txt, ctx: ("\t" not in (txt or ""), "tabs"), enforce=lambda txt, ctx: _ats_plain_text(txt), ), Guideline( id="buzzword_scrub", description="Remove common buzzwords", condition=lambda ctx: True, validate=lambda txt, ctx: (not any(bw in (txt or "").lower() for bw in _BUZZWORDS), "buzz"), enforce=lambda txt, ctx: _scrub_buzzwords(txt), ), Guideline( id="verb_strengthen", description="Strengthen weak bullet openers", condition=lambda ctx: True, validate=lambda txt, ctx: (True, "noop"), enforce=lambda txt, ctx: _strengthen_action_verbs(txt), ), Guideline( id="remove_first_person", description="Remove first-person pronouns on bullets", condition=lambda ctx: True, validate=lambda txt, ctx: (not re.search(r"^\s*[-•]\s*(i|my|we)\b", txt or "", re.I | re.M), "pronouns"), enforce=lambda txt, ctx: _remove_first_person(txt), ), Guideline( id="uk_normalization", description="Normalize UK currency/percent spacing", condition=lambda ctx: True, validate=lambda txt, ctx: (True, "noop"), enforce=lambda txt, ctx: _enforce_uk_habits(txt), ), Guideline( id="no_invented_skills", description="Prevent skills not evidenced in profile", condition=lambda ctx: True, validate=_no_invented_skills, enforce=lambda txt, ctx: txt, # log-only to avoid false positives ), ] def build_cover_rules() -> List[Guideline]: return [ Guideline( id="exact_length", description="Enforce exact target length when provided", condition=lambda ctx: bool(ctx.get("target_len")), validate=lambda txt, ctx: (len(txt or "") == int(ctx.get("target_len", 0)), "len"), enforce=lambda txt, ctx: _enforce_exact_length(txt, int(ctx.get("target_len", 0))), ), Guideline( id="ats_plain_text", description="Normalize bullets/tabs for ATS", condition=lambda ctx: True, validate=lambda txt, ctx: ("\t" not in (txt or ""), "tabs"), enforce=lambda txt, ctx: _ats_plain_text(txt), ), Guideline( id="buzzword_scrub", description="Remove common buzzwords", condition=lambda ctx: True, validate=lambda txt, ctx: (not any(bw in (txt or "").lower() for bw in _BUZZWORDS), "buzz"), enforce=lambda txt, ctx: _scrub_buzzwords(txt), ), ] def apply_resume_guidelines(text: str, ctx: Dict[str, Any]) -> Tuple[str, List[str], List[str]]: engine = GuidelineEngine(build_resume_rules()) return engine.check_and_enforce(text, ctx) def apply_cover_guidelines(text: str, ctx: Dict[str, Any]) -> Tuple[str, List[str], List[str]]: engine = GuidelineEngine(build_cover_rules()) return engine.check_and_enforce(text, ctx)