JAA-ATS-Tool / scripts /verify_max_ats_coverage.py
saitejatirunagari's picture
feat: non-destructive tailoring + reliable PDF + persistent side panel + history (Phase 8)
8155d78
Raw
History Blame
6.89 kB
"""
Hard regression for the LIVE FAILURE: internal 96% but Jobalytics 54% (26/46,
Hard Skills 24/43) on an Amazon/Product-Manager-type JD.
This locks in the fix: in Maximum ATS Mode the EXPORTED DOCX physically contains
the INCLUDABLE external keywords across Skills + Summary + Experience, driven by
external coverage β€” NOT by the internal score β€” while credentials / fake
seniority stay blocked. Deterministic (StubProvider, no API keys).
PHASE 8 (R17) NON-DESTRUCTIVE NOTE β€” why the DOCX floor is < 90%:
Tailoring is now APPEND-ONLY by default: the candidate's real role titles and
existing bullets are preserved VERBATIM (the user's explicit Phase 8 ask), so
we NO LONGER stuff keywords into existing bullets. The honest DOCX vehicles
are therefore (a) the clean, recruiter-credible Skills section (capped at ~28
items / one line per category β€” the Phase 7 anti-"Core Competencies-dump" fix),
(b) a Summary augmentation, and (c) <=3 appended bullets per role. A JD whose
includable terms are mostly the same Skills bucket cannot fit them all in a
CLEAN Skills line without either repeated headers (Phase 7 banned) or a 15+
separator dump line (anti-spam banned). That is an intended consequence of the
user's two constraints (clean resume + don't rewrite my history) β€” NOT a bug.
The LaTeX path (the user's PREFERRED output) is not subject to the DOCX caps
and still reaches ~88% (see scripts/verify_latex_resume.py). The floor below is
the honest non-destructive DOCX floor; honesty (no fabrication) stays strict.
"""
import os, sys, io, shutil
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
sys.path.insert(0, os.path.abspath("."))
import src.candidate_vault as _cv
_cv._VAULT_PATH = "data/_test_vault_maxcov.json"
if os.path.exists(_cv._VAULT_PATH):
os.remove(_cv._VAULT_PATH)
import shutil as _sh
src_pdf = r"C:\Users\Nxtwave\Desktop\resume\Saiteja_Tirunagari_Resume A 26 - Copy.pdf"
dst = "data/resume/resume.pdf"
os.makedirs("data/resume", exist_ok=True)
if not os.path.exists(dst) and os.path.exists(src_pdf):
_sh.copyfile(src_pdf, dst)
from src.resume_parser_v2 import parse_resume_pdf
from src.providers import StubProvider
from src.jobalytics_repair import repair_with_external_feedback
from src.resume_customizer import _read_docx_text
from src.ats_scorer import _kw_in_text
from src.fit_gate import (READY, READY_REVIEW, READY_MAX_ATS_95_PLUS,
READY_90_PLUS_EXTERNAL_ALIGNED, BELOW_TARGET_REPAIRABLE,
NEEDS_USER_CONFIRMATION)
ok = True
def check(name, cond, detail=""):
global ok; ok = ok and cond
print(f" [{'PASS' if cond else 'FAIL'}] {name} {detail}")
# Normal PM/Product terms a real AI/PM JD wants (from the live screenshot) β€” all
# interview-supportable, none are credentials.
INCLUDABLE = [
"product strategy", "ai", "influence", "acceptance criteria", "backlog",
"program management", "product management", "product marketing",
"business development", "global teams", "diverse partners",
"corporate travel", "expense management", "payments", "roadmap",
"stakeholder management", "requirements", "user stories", "prioritization",
"data-driven decisions", "metrics", "experimentation", "product discovery",
"go-to-market", "saas", "b2b",
]
# Must NEVER be fabricated.
BLOCKED = ["CISSP", "12+ years"]
jd = open("tests/fixtures/jds/generic_pm_3_7yrs.txt", encoding="utf-8").read()
jd += ("\n\nResponsibilities: product strategy, roadmap, backlog, user stories, "
"acceptance criteria, prioritization, stakeholder management, requirements, "
"program management, product marketing, business development, global teams, "
"diverse partners, corporate travel, expense management, payments, metrics, "
"experimentation, data-driven decisions, go-to-market, product discovery. "
"Hard skills: AI, SaaS, B2B, influence. 12+ years required. CISSP preferred.")
base = parse_resume_pdf(dst)
job = {"title": "Product Manager", "company": "Amazon", "description": jd, "_raw_assessment": {}}
fb = ("Match score 54%. Resume has 26 out of 46 keywords. Hard Skills 24 of 43. "
"Missing keywords: " + ", ".join(INCLUDABLE + BLOCKED) + ".")
res = repair_with_external_feedback(job, feedback_text=fb, provider=StubProvider(),
base_resume=base, maximum_ats_mode=True,
output_dir="data/output/resumes/_maxcov_test")
print("repair status:", res.get("status"), "scores:", res.get("scores"),
"after_cov:", res.get("after_coverage", {}).get("pct"))
check("no error", "error" not in res, res.get("error", ""))
# THE core assertion: the EXPORTED DOCX physically contains the includable terms
# it can honestly hold under non-destructive + clean-Skills constraints. The
# non-destructive DOCX floor is >=55% (the rest, if any, lives in the LaTeX path
# which is uncapped β€” see module docstring). Honesty stays strict below.
_NON_DESTRUCTIVE_DOCX_FLOOR = 55
txt = _read_docx_text(res["resume_path"]).lower()
found = [t for t in INCLUDABLE if _kw_in_text(t, txt)]
missing = [t for t in INCLUDABLE if t not in found]
pct = int(round(100 * len(found) / len(INCLUDABLE)))
check(f"exported DOCX covers >={_NON_DESTRUCTIVE_DOCX_FLOOR}% of includable PM "
"terms (non-destructive floor; LaTeX path covers more)",
pct >= _NON_DESTRUCTIVE_DOCX_FLOOR,
f"{len(found)}/{len(INCLUDABLE)} = {pct}% missing={missing}")
# Honesty: credentials / fake seniority never fabricated.
check("CISSP NOT in exported DOCX", "cissp" not in txt)
check("no fabricated '12+ years'", "12+ years" not in txt and "12 years" not in txt)
check("CISSP surfaced as blocked", "CISSP" in res.get("blocked_terms", []))
# Status must NOT be a stuck NEEDS_REPAIR β€” external coverage drives it.
good_status = {READY, READY_REVIEW, READY_MAX_ATS_95_PLUS,
READY_90_PLUS_EXTERNAL_ALIGNED, BELOW_TARGET_REPAIRABLE,
NEEDS_USER_CONFIRMATION}
check("status driven by external coverage (not stuck NEEDS_REPAIR)",
res.get("status") in good_status, res.get("status"))
# Coverage report shows where each term landed.
cov = res.get("coverage_report", {})
kw = cov.get("keywords", [])
check("coverage_report lists per-term sections", bool(kw),
f"{len(kw)} terms reported")
secs = {k.get("section") for k in kw if k.get("placed_in_resume") or k.get("found_in_export")}
check("terms placed in Skills AND Experience (multi-section)",
("Skills" in secs and any("Experience" in s for s in secs)) or len(secs) >= 2,
f"sections={secs}")
print("\n" + ("\u2713 LIVE-FAILURE REGRESSION PASSES" if ok else "\u2717 SOME FAILED"))
shutil.rmtree("data/output/resumes/_maxcov_test", ignore_errors=True)
if os.path.exists(_cv._VAULT_PATH):
os.remove(_cv._VAULT_PATH)
sys.exit(0 if ok else 1)