Spaces:
Sleeping
Agent Context β Job Automation Agent
Read this first. Everything a new agent (Claude/Cursor) or developer needs: the architecture, the hard-won design decisions, what works, what's open, and where to start. If this contradicts the code, the code wins β but update this doc when you finish.
Last updated: 2026-06-20 (HEAD ef247ae)
Active branch: main (single branch, no PRs)
Remotes: origin (GitHub) + hf (Hugging Face Space) β both at ef247ae
Recent commits: ef247ae ext result persistence Β· f5dd879 External ATS Feedback Repair Mode
1. Mission
Help a Product Manager job-seeker get ATS-optimized, honestly-scored resumes, two ways from ONE shared Python pipeline:
- Streamlit web app (batch): upload resume β scrape PM jobs from many platforms β assess β tailor a per-job resume β score β log to Google Sheets.
- Chrome extension (per-job, in-browser): open ANY job listing β click Run β the extension grabs the JD + sends it with the stored resume to the same pipeline exposed as a JSON API β get a tailored DOCX/PDF download. Built to avoid IP-blocking from bulk scraping.
The HF Space is the single source of truth: one codebase serves the Streamlit app AND the extension API, so the score is identical everywhere (proven by a parity test).
Live: https://huggingface.co/spaces/saitejatirunagari/JAA-ATS-Tool
API base: https://saitejatirunagari-jaa-ats-tool.hf.space
Endpoints: /api/health Β· /api/generate Β· /api/repair-with-feedback
Repo: https://github.com/saitejatiru/JAA-ATS-Tool
Stack: Python 3.11 + Streamlit + FastAPI + Chrome MV3 extension + multi-LLM
provider chain (Kimi 2.6 / NVIDIA-hosted, OpenAI-compatible) + python-docx +
reportlab + NestJS ever-jobs sidecar (optional) on HF Spaces (Docker).
2. THE most important thing: the honesty / anti-circular scoring model
This project spent days on one trap and the current design exists to prevent it.
The trap: the original scorer extracted keywords from our own narrow taxonomy, injected exactly those, and scored against the same list β always ~95% internally, but Jobalytics/Simplify reported ~55-73% on the same resume. The score was self-graded and fake.
The current model (do not regress this):
- Two scores, not one:
- JD Match (internal) β weighted coverage of the JD's requirements
(
ats_scoring_v2.score_jd_match). - Independent validation β a separate, stricter scorer
(
ats_validator.validate_resume) that shares NO logic with the generator: exact/alias presence only (no "plausible" credit), evidence-weighted (a term in the Skills list but NOT in an experience bullet earns half credit), and strict seniority measured from the BASE resume's years (so an injected "12+ years" can't game it). It scores the re-parsed exported file.
- JD Match (internal) β weighted coverage of the JD's requirements
(
- A resume is only READY (downloadable as good) if BOTH internal AND independent β₯ 90 AND ATS readability β₯ 90 AND parse-validation passed.
- Auto-repair loop weaves missing/skills-only terms into bullets (real evidence) until both scores clear 90 β it cannot pass by stuffing the Skills list, because the independent validator only fully credits evidenced terms.
- Per-job scores legitimately vary. A great-fit PM JD β 90s. A JD full of domain terms the candidate can't evidence (security tooling, AR/fintech, backend eng) β honestly lower. This is correct, not a regression.
Anti-cheat is enforced by scripts/verify_anticheat.py (must stay green):
PM JD β READY bothβ₯90; cybersec PM β security terms flagged HIGH, not
auto-claimed; backend-eng JD β not CLEAN (independent < 90); 12-yr JD β
seniority fails, download blocked; score comes from parsed export; truncating
the file drops the score.
Never loosen the independent validator to make numbers look better, and never inject fake employers/degrees/certs/dates/seniority.
3. Candidate Fit Expansion + AUTO_AGGRESSIVE (how keywords get included)
The resume is treated as a base profile, not the full truth. We include JD terms aggressively where plausible, and gate the genuinely risky ones.
src/candidate_fit.py classifies every JD requirement β a risk severity:
| Severity | What | Action |
|---|---|---|
LOW_RISK_AUTO_INCLUDED |
common PM/product/analytics/agile craft, normal tools/methods/responsibilities, real soft skills, anything already in the resume | auto-include |
MEDIUM_RISK_REVIEW_RECOMMENDED |
domain/industry terms & plausible tools not in the base resume | auto-include + flag review |
HIGH_RISK_NEEDS_CONFIRMATION |
specialized platforms (SIEM/SOAR), compliance/regulatory, hands-on engineering hard skills, seniority-sensitive | NOT auto-included; if a job needs them to hit 90 β NEEDS_USER_INPUT |
BLOCKED_DO_NOT_INCLUDE |
degrees/certs/licenses not held, fakes, seniority jumps, deep-tech specialties | excluded |
config.AUTOMATION holds automation_mode="auto_aggressive", review_policy,
download_policy. Generation auto-includes LOW+MEDIUM (weave/skills/repair all
gated to exclude HIGH+BLOCKED).
Maximum ATS Mode (User-Confirmed Skill Expansion): classify_fit(..., maximum_ats_mode=True) promotes the curated config.MAXIMUM_ATS_SAFE_TERMS
(normal PM/Product/AI/SaaS/B2B/agile vocabulary) to user-confirmed (LOW) β AFTER
the hard-block checks, so credentials/seniority/engineering/specialized still
block or ask first. Flows via the job dict (_maximum_ats_mode,
_confirmed_terms) into _generate_resume_v4, and via params through
jobalytics_repair. Targets external 95 / floor 90 (config.MAXIMUM_ATS); the
repair loop iterates while LOW/MEDIUM gaps remain. Confirmed terms persist to the
vault (confirm_expansion_terms). The boundaries in Β§2 are NEVER relaxed by it.
β The internal score is NOT a valid external signal. Internal taxonomy
coverage can be 96% while Jobalytics is 54% (it extracts a broader 40-46 term
set). So Maximum ATS Mode is driven by EXTERNAL coverage: src/external_ats.py
builds a broad Jobalytics-style expected set; resume_customizer. _maximize_external_coverage() honesty-gates each term then physically
guarantees the includable ones into the exported DOCX (Skills verbatim + woven
bullets) and measures coverage from the re-parsed export. Status becomes
READY_MAX_ATS_95_PLUS / READY_90_PLUS_EXTERNAL_ALIGNED / BELOW_TARGET_REPAIRABLE.
_build_skill_pool no longer caps off includable terms in max mode (cap β 200).
Regression: scripts/verify_max_ats_coverage.py (the internal-96/external-54 bug).
Status system (src/fit_gate.py): READY_90_PLUS /
READY_90_PLUS_REVIEW_RECOMMENDED / READY_95_EXTERNAL_ALIGNED (feedback repair
only: internal AND independent β₯ 90, pasted gaps mostly resolved, no review flags β
fabrication cannot reach it) / NEEDS_REPAIR / NEEDS_USER_INPUT /
NOT_ELIGIBLE_LOW_FIT / PARSE_FAILED.
Maximum ATS statuses: READY_MAX_ATS_95_PLUS / READY_90_PLUS_EXTERNAL_ALIGNED
/ NEEDS_USER_CONFIRMATION / BELOW_TARGET_REPAIRABLE.
Quality flags: CLEAN_90_PLUS / AGGRESSIVE_90_PLUS /
REVIEW_REQUIRED_90_PLUS / WEAK_90_INTERNAL_ONLY (internal β₯90 but independent <90).
Candidate Experience Vault (src/candidate_vault.py, data/candidate_vault.json,
gitignored/per-user): accumulates terms across jobs. Only user_confirmed +
resume_original are treated as fully safe; inferred/jd_expansion stay
reviewable β the system never silently promotes a risky term. confirm_terms()
lets the user lock a term safe or blocked; the fit classifier consults it first.
4. β Reversals from older versions of this doc (history that changed)
- Skills section: RE-ADDED. Older docs said "NO Core Competencies/Skills
section." That was wrong β research showed a dedicated, standard-headed Skills
section is the #1 ATS keyword vehicle (Jobalytics/Resume Worded parse it).
Resume.skills+resume_renderer._write_skillsrender a categorized SKILLS section (Tools & Analytics / Methodologies / Domains / Core Competencies), placed after KEY ACHIEVEMENTS, β€12 items per line (multi-line if more). - Extraction is SKILLS-ONLY + LLM-unioned.
extract_jd_keywordskeeps only recognized skills (no "any noun"); the v4 LLM also returns ajd_skillsarray (Jobalytics-style) that is unioned in. No prose-noun garbage. - Provider-independent. Generation no longer assumes one model β see Β§6.
- The
_assert_no_dump_footerpostcondition still bans raw keyword-dump footers, but the categorized Skills section is allowed.
5. End-to-end flows
Web app (batch)
ui.py wizard β scrape (src/scrapers/* + ever-jobs) β geo_filter (location)
β dedup β job_assessor β ResumeCustomizer.customize_for_jobs β
_generate_resume_v4 (provider chain + repair + scoring + status) β render
DOCX/PDF β Google Sheets / Excel / CSV / run history (hf_storage syncs to HF
Dataset on save). Batch table shows status, internal+independent scores, risk,
download gating. UI startup calls hf_storage.sync_down() once per session.
Extension (per-job)
extension/ (MV3) β content.js extracts JD (generic + LinkedIn/Naukri/Indeed) β
popup Run β background.js POSTs multipart to POST /api/generate β
api_server.generate_resume_for_api β
ResumeCustomizer._run_provider_chain(..., base_resume_override=...) (same path)
β returns base64 DOCX/PDF + scores/status β popup shows scores + Download.
Improve with ATS feedback (External Feedback Repair Mode): after a resume
exists, user pastes Jobalytics/Simplify feedback β popup Improve β
background.js REPAIR β POST /api/repair-with-feedback β
jobalytics_repair.repair_with_external_feedback (every missing keyword
risk-classified; BLOCKED + unconfirmed HIGH never added; re-render β re-parse β
re-score) β updated scores/status + added / unresolved-high / blocked terms +
base64 DOCX/PDF. No ATS logic in extension JS.
Result persistence: generated/repaired results are cached in
chrome.storage.local (ats_results, keyed by tab URL). Reopening the popup on
the same job link restores scores, Download, and the Improve section (pruned to
10 most-recent jobs). Reload the unpacked extension after pulling extension/
changes from GitHub.
External ATS Feedback Repair (backend detail)
src/jobalytics_repair.py:
parse_external_feedback(text)β extracts external score + missing/matched keywords from a freeform paste; drops prose tokens when there's no real list.repair_with_external_feedback(...)β runsregenerate_from_jobalytics(full v4 pipeline injobalytics_repairmode with pasted addable keywords merged into the include pool), then derives added / review-flag / unresolved-high / blocked lists and final status (may upgrade toREADY_95_EXTERNAL_ALIGNED). Verified byscripts/verify_feedback_repair.py(CISSP blocked, SIEM unresolved-high).
6. LLM provider abstraction (model-independent)
src/providers.py: LLMProvider base, OpenAICompatProvider (Kimi/NVIDIA β
all OpenAI-compatible cfgs), StubProvider (deterministic, offline floor +
tests). build_provider_chain() reads config.LLM_GENERATION.provider_order.
- Every model response is schema-validated (
validate_responseagainstJD_ANALYSIS_SCHEMA/RESUME_TAILORING_SCHEMA); quality βok | failed_schema | provider_error. Schema-failed output can never become READY β it forces fallback to the next provider / deterministic path. src/provider_prompts.pyloads provider-specific templates fromprompts/({task}_{claude|kimi|nvidia}.txt).LLMClient.tailor_resume_v4(..., provider_family=...)selects the prompt. Any stub/test that monkeypatchestailor_resume_v4must accept**kwargs(it takesprovider_family).- Pipeline records
provider_used+provider_response_qualityon the report.
7. File map (current)
Entry points
| File | Purpose |
|---|---|
api_server.py |
HF Space entrypoint (start.sh runs it). FastAPI on :7860: /api/health, /api/generate, /api/repair-with-feedback; launches Streamlit subprocess on :8501 and reverse-proxies everything else (HTTP via httpx + WS for /_stcore/stream). Imports src/ β no ATS logic of its own. |
ui.py |
Streamlit web app (~2400 lines): wizard, results, batch table, vault controls. |
start.sh |
ever-jobs sidecar (optional) β exec python api_server.py. |
Dockerfile |
HF build. |
ATS pipeline v2 (src/)
| File | Purpose |
|---|---|
jd_analyzer.py |
analyze_jd β structured JDRequirements (titles, required/preferred hard skills, tools, responsibilities, domains, certs, education, soft, seniority) each with importance/source_phrase/aliases/placement. Deterministic floor + optional LLM enrichment. |
candidate_fit.py |
Fit/severity classification (Β§3): classify_all_fit, severity, auto_terms, high_risk_terms. |
fit_gate.py |
assess_job_fit_for_90 + status constants. |
ats_scoring_v2.py |
score_jd_match (weighted: 35 must-have/20 responsibilities/15 tools/10 title+domain/10 seniority/5 certs/5 soft + penalties) + score_ats_readability. |
ats_validator.py |
Independent evidence-weighted validator (anti-circular). |
ats_report.py |
build_ats_report (explanation: scores, strong/weak, gaps, formatting, recommendations) + reconcile_missing_keywords. |
candidate_vault.py |
Persistent learned-skills vault. |
jobalytics_repair.py |
classify_jobalytics_keywords, regenerate_from_jobalytics, parse_external_feedback, repair_with_external_feedback β honest external-checker feedback repair (Β§5). |
hf_storage.py |
Best-effort HF Dataset sync for Streamlit run history/resumes/reports (private dataset; sync_down on UI startup, push_run after each run). |
ats_scorer.py |
Base scorer/helpers still used: extract_jd_keywords (skills-only), _kw_in_text, _phrase_in_text, _strip_keyword_spam (drops 15+-separator dump lines), conservative_display_score, PM_SKILL_TAXONOMY. |
resume_model.py |
Resume/Role/Education/Contact dataclasses (+ skills). |
resume_parser_v2.py |
parse_resume_pdf / parse_resume_pdf_cached (PDF β Resume). |
resume_renderer.py |
Canonical DOCX renderer incl. the SKILLS section. |
resume_customizer.py |
Heart. _generate_resume_v4 (parse β provider tailor β backfill β weave β skills β render β parse-validate β score internal+independent β auto-repair β€3 β status/quality/download gating), _run_provider_chain, _validate_parsed_resume, customize_for_jobs. |
llm_client.py |
tailor_resume_v4, analyze_jd_requirements, judge_evidence, repair_resume_v4, jobalytics_repair_v4 (all OpenAI-compatible, cfg-driven). |
providers.py / provider_prompts.py |
Provider abstraction (Β§6). |
geo_filter.py |
Drops jobs outside selected locations (respects Remote/Worldwide). |
pdf_writer.py |
DOCXβPDF (reportlab on Linux/HF). |
job_assessor.py, gsheets.py, excel_reporter.py, run_history.py, job_history.py |
Assessment, Sheets, Excel, history, dedup. |
Extension (extension/)
| File | Purpose |
|---|---|
manifest.json |
MV3; permissions: storage, downloads, activeTab, scripting; <all_urls>. |
content.js |
JD extraction: generic + LinkedIn/Naukri/Indeed + document.title fallback; fast (single short MutationObserver wait). |
popup/popup.html + popup.js |
Run (enabled immediately), status, scores, Download (always shown, labeled by score), Improve with ATS feedback panel, result restore from chrome.storage.local, β Options. Download in popup (Blob + <a download>). |
background.js |
Thin service worker: GENERATE β /api/generate, REPAIR β /api/repair-with-feedback. Reads resume/API settings from chrome.storage.local. NO ATS/LLM logic. |
options/ |
Upload resume once (β base64 in chrome.storage.local) + API URL + token. |
Config
config.py: ASSESSMENT_MODELS (with tailor flags), LLM_GENERATION
(provider_order, fallback flags), AUTOMATION (review/download policy).
Secrets in .env / HF Space secrets: LLM keys, API_SECRET_TOKEN, HF_DATASET_REPO
(for hf_storage), EVER_JOBS_API_URL, Google. Never committed.
Also: CONTACT_LOCATION (default Hyderabad, Telangana, India Β· Open to relocate)
for resume header address/location ATS compliance.
Tests / scripts
| File | Purpose |
|---|---|
scripts/verify_anticheat.py |
Anti-circular suite β keep green. |
scripts/verify_feedback_repair.py |
External feedback repair: parse + honest weave; CISSP blocked, SIEM unresolved-high. |
scripts/verify_maximum_ats.py |
Maximum ATS Mode: PM/AIβuser-confirmed; certs/seniority/eng still gated; coverage improves; 65%βrepairable. |
scripts/verify_max_ats_coverage.py |
Internal-96/external-54 regression: exported DOCX covers β₯90% includable PM terms; creds/seniority excluded; status by external coverage. |
scripts/verify_90_pipeline.py |
Full fitβrepairβstatus pipeline (stub floor). |
scripts/verify_scoring_v2.py |
jd_analyzer + evidence + scoring + report + feedback loop. |
scripts/evaluate_model_providers.py |
Run same jobs through each provider (Kimi/NVIDIA) β ready rate, avg independent, schema errors. Needs real keys. |
scripts/consistency_test.py |
Same JD Γ3 per provider β variance. |
scripts/run_20_job_validation.py |
20-job end-to-end + validation packages. Needs real keys. |
tests/test_api_parity.py |
Proves API path == web-app path (identical scores). |
tests/test_api_health.py |
/api/health + token gate. |
tests/fixtures/jds/*.txt |
Real JDs incl. backend_engineer, senior_pm_10yrs (anti-cheat). |
8. Current status & known gotchas
- Phase 6 (Chrome extension) is COMPLETE and live. End-to-end verified on a real Kimi/NVIDIA run: a good-fit job returned READY_90_PLUS, independent 98, and a downloadable DOCX. Parity test passes.
- External ATS Feedback Repair Mode is wired end-to-end (
f5dd879): backend/api/repair-with-feedback+ extension Improve panel. Requires the Space to be rebuilt/running with that commit β if Improve fails with 404, the endpoint isn't deployed yet. - Extension result persistence is live (
ef247ae): reopening the popup on the same job URL restores the last generated/repaired result. User must reload the unpacked extension after pulling from GitHub. - Per-job variance is expected (see Β§2): borderline jobs land NEEDS_REPAIR /
~73-85 on Jobalytics β that's honest, not a bug. Feedback repair pushes toward
90 only where missing keywords are plausible-and-weavable; HIGH/BLOCKED terms
correctly stay out β
NEEDS_USER_INPUT. - MV3 gotcha: service workers have no
URL.createObjectURLβ downloads are done in the popup, not the service worker. - Proxy gotcha:
api_serverstripscontent-encoding/content-lengthwhen proxying Streamlit (httpx already decompresses) β otherwise pages render blank. - Deploy gotcha: pushing to
hfrebuilds the Space (~2-15 min for Docker) and causes transient 404/405s while the container is down. Prefer pushing extension-only changes tooriginonly (no Space rebuild needed) β but both remotes may end up at the same commit if you push everything to both. - HF Space is private: unauthenticated health checks from outside may return HF gateway 404 even when the Space is fine for token-authenticated extension calls.
- Local
import uifails on the dev machine due to a pre-existing pandas/numpy binary mismatch β unrelated to our code; HF's clean deps are fine. - ever-jobs sidecar fails to start on HF unless
EVER_JOBS_API_URLpoints to an externally hosted sidecar; known/non-blocking; dedicated scrapers still work. - Streamlit batch persistence:
hf_storage.pysyncs run history/resumes/CSV to a private HF Dataset so data survives Space container restarts (needsHF_DATASET_REPO+ write token in Space secrets). - Security: do not store HF write tokens in
.git/configremote URLs β usehuggingface-cli loginor a credential helper instead.
Open / next
- Run
evaluate_model_providers.py+run_20_job_validation.pywith real Kimi/NVIDIA keys (user's environment) β the live provider validation. - Integrate a no-block job source for HF batch scraping (e.g. Adzuna/Jooble API or
jobdrop) β HTML scrapers on datacenter IPs get blocked.
9. Conventions (follow these)
- Update
HISTORY.md+README.mdon every change (standing user rule). - Commit/push only when asked. Conventional Commits (
fix(scope): β¦) + aCo-Authored-Bytrailer when AI-assisted. Singlemainbranch, no rebases. - Push extension-only changes to
originonly; push pipeline/API/HF changes to bothoriginandhf. - Secrets never committed (
.env,google_*.json, resume PDFs,data/). - Honesty over false confidence. If a fix is partial, say so. If our score > what a real checker would give, say so. Never fake keywords or loosen the independent validator to inflate numbers.
- Gold reference resume:
C:\Users\Nxtwave\Desktop\resume\Saiteja_Tirunagari_Resume A 26 - Copy.pdf. - Test vaults are isolated in regression scripts (temp paths) β don't pollute the real vault.
10. Where to start
- Low/odd ATS scores: read
ats_scoring_v2.py+ats_validator.py(internal vs independent), thencandidate_fit.py(what got included), then the_generate_resume_v4repair loop. Runverify_anticheat.py+verify_90_pipeline.py. - Extension issues:
extension/(manifest β content.js β popup.js β background.js) +api_server.py. Remember MV3 download + proxy gotchas (Β§8). Improve broken? Check Space has/api/repair-with-feedback. Results lost on reopen? Check extension was reloaded after persistence commit + same job URL. - Feedback repair / 95% path:
jobalytics_repair.pyβapi_server.pyrepair endpoint β extensionREPAIRhandler. Runverify_feedback_repair.py. Never loosen validator or auto-add HIGH/BLOCKED terms. - Provider/model work:
providers.py+provider_prompts.py+prompts/+llm_client.py. Keep schema validation; keep stubs**kwargs-tolerant. - New JD-keyword behavior:
jd_analyzer.py(extraction) andcandidate_fit.py(inclusion). Don't reintroduce "any noun" extraction or fake injection.
11. Quick commands
# Regressions (deterministic, no keys)
PYTHONPATH=. python scripts/verify_anticheat.py
PYTHONPATH=. python scripts/verify_90_pipeline.py
PYTHONPATH=. python scripts/verify_scoring_v2.py
PYTHONPATH=. python scripts/verify_feedback_repair.py
PYTHONPATH=. python -m pytest tests/test_api_parity.py tests/test_api_health.py -q
# Provider eval (needs real Kimi/NVIDIA keys in .env)
PYTHONPATH=. python scripts/evaluate_model_providers.py
PYTHONPATH=. python scripts/run_20_job_validation.py
# Run the API locally (serves /api + proxies Streamlit)
python api_server.py # http://localhost:7860
# Deploy
git push origin main # always
git push hf main # ONLY for pipeline/API/Space changes (rebuilds Space)
# Live checks (Space must be Running; private Space may 404 without auth)
# GET β¦/api/health β {"status":"ok"}
# POST β¦/api/repair-with-feedback β 405 on GET means route exists; POST needs token + multipart
End of context. Code is the source of truth β update this doc when you finish a task.