Spaces:
Sleeping
Sleeping
File size: 31,020 Bytes
197c01c c1d2eaa 197c01c 8155d78 c1d2eaa 8155d78 197c01c c1d2eaa 197c01c c1d2eaa 197c01c c1d2eaa 197c01c b3704a6 197c01c c1d2eaa 197c01c c1d2eaa 197c01c c1d2eaa 197c01c c1d2eaa 197c01c c1d2eaa 197c01c c1d2eaa 22120b8 c1d2eaa 22120b8 b3704a6 7cad0b6 7759bfb 8155d78 c1d2eaa b3704a6 c1d2eaa b3704a6 c1d2eaa 22120b8 c1d2eaa 197c01c c1d2eaa 197c01c c1d2eaa 197c01c c1d2eaa b3704a6 197c01c c1d2eaa b3704a6 c1d2eaa 197c01c b3704a6 197c01c c1d2eaa 197c01c c1d2eaa 197c01c c1d2eaa 197c01c c1d2eaa 197c01c c1d2eaa b3704a6 c1d2eaa 197c01c c1d2eaa b3704a6 c1d2eaa b3704a6 c1d2eaa 197c01c c1d2eaa b3704a6 197c01c c1d2eaa b3704a6 7cad0b6 7759bfb c1d2eaa 197c01c c1d2eaa b3704a6 c1d2eaa b3704a6 c1d2eaa b3704a6 c1d2eaa b3704a6 9e9393d b3704a6 c1d2eaa 9e9393d 197c01c c1d2eaa 197c01c c1d2eaa 197c01c c1d2eaa 4f65a1d 197c01c c1d2eaa 197c01c c1d2eaa b3704a6 c1d2eaa 197c01c c1d2eaa 197c01c c1d2eaa b3704a6 c1d2eaa 197c01c c1d2eaa 197c01c c1d2eaa 197c01c c1d2eaa 197c01c b3704a6 197c01c c1d2eaa | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 | # 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-23 (Phase 8)
Active branch: `main` (single branch, no PRs)
Remotes: `origin` (GitHub) + `hf` (Hugging Face Space)
Recent work: Phase 8 (non-destructive tailoring + reliable PDF + persistent side
panel + history, extension v1.5.0) Β· `7759bfb` Phase 7 (LaTeX + placement + resilient Run)
---
## 1. Mission
Help a Product Manager job-seeker get **ATS-optimized, honestly-scored** resumes,
two ways from ONE shared Python pipeline:
1. **Streamlit web app (batch):** upload resume β scrape PM jobs from many
platforms β assess β tailor a per-job resume β score β log to Google Sheets.
2. **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**.
- **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).
**LaTeX resume path (`src/latex_resume.py`):** the candidate can supply their
resume as LaTeX (clean PDF design they control). When `resume_latex` is present it
is PRIORITISED over the uploaded PDF for both keyword matching and output.
`latex_to_text()` (pylatexenc, regex fallback) feeds the same `external_ats` +
`candidate_fit` honesty gate as the DOCX path, PLUS `_specialty_hit()` β a
phrase-level guard so multi-word JD grams (`cuda kernel programming`) can't
smuggle a blocked engineering/credential token past the exact-match classifier.
`inject_keywords()` splices a compile-safe `\textbf{Core Competencies:}` block
before `\end{document}` (idempotent, escapes specials); `compile_latex_to_pdf()`
uses Tectonic (preferred)/latexmk/pdflatex with shell-escape disabled. With no
engine installed it still returns the injected `.tex` + the (text-based) coverage
report. API: `/api/generate` + `/api/repair-with-feedback` accept `resume_latex`
and branch to `_generate_from_latex` / `_repair_from_latex` (return `pdf_b64` +
`tex_b64` + coverage report; status by external coverage). Extension v1.3.0 adds a
**Resume LaTeX** textarea (priority over PDF) and Download PDF / Download .tex.
Regression: `scripts/verify_latex_resume.py`. Tectonic is installed in the
Dockerfile; `pylatexenc` is in `requirements.txt`.
**Recruiter-grade placement + clean inputs + resilient Run (Phase 7):** the live
output was a wall of repeated "Core Competencies:" lines full of LinkedIn/Simplify
UI junk. Fixed across four axes (all honesty-preserving):
- **Clean JD (R13):** `extension/content.js::extractGenericJD` strips a CLONE
(`stripChrome`: nav/aside/header/footer/button/overlay + `simplify`/`jobalytics`/
`jobscan`/`teal`/`__extension` nodes) and `scrubOverlayLines` drops CTA lines;
`external_ats._is_ui_noise()` (wired into `_is_term_like`) rejects UI/CTA/
marketing n-grams so page chrome never becomes a keyword.
- **DOCX distribution (R14):** `resume_renderer._write_skills` emits ONE capped
line per category (`_PER_CATEGORY_CAP=10`, `_SKILLS_TOTAL_CAP=28`; no more
`PER_LINE=12` repeated headers); `_build_skill_pool`/repair cap is
`_SKILLS_DISPLAY_CAP=26` (was 200); surplus includable terms are redirected into
the Summary (`_append_summary_terms`) + Experience (`_force_weave_into_bullets`),
not extra skills lines.
- **LaTeX distribution (R14):** `latex_resume.inject_keywords` distributes terms
across a Summary sentence + woven Experience `\item`s + ONE competencies line;
`_remove_injected_block` strips every fragment (idempotent).
- **Resilient Run (R15):** `extension/background.js` owns generation and persists
`running`β`done`/`error` to `ats_results` keyed by normalized URL; the popup
restores spinner/result on open + live-refreshes via `chrome.storage.onChanged`.
Closing the popup no longer loses the run (extension v1.4.0).
Regressions: `verify_clean_jd_extraction.py`, `verify_skills_distribution.py`,
`verify_resilient_run.py` (+ existing `verify_latex_resume.py` /
`verify_max_ats_coverage.py` still pass).
**Trust + usability (Phase 8, extension v1.5.0):** four live-use fixes, all
honesty-preserving:
- **Reliable PDF (R16):** the HF/Linux DOCX flow never wrote a `.pdf` sidecar so
only DOCX downloaded. `api_server` now calls `pdf_writer.docx_to_pdf` on both
the generate and repair DOCX paths (reportlab off-Windows) β `pdf_b64` always
populated. The LaTeX path returns a `pdf_error` (`engine_missing` vs
`latex_error` + `compile_log`) and STILL returns `tex_b64` on compile failure
(200, not 500); `Dockerfile` has a hard `RUN tectonic --version` gate.
- **Non-destructive tailoring is the DEFAULT (R17):** `NON_DESTRUCTIVE_DEFAULT=True`
+ `_apply_non_destructive` rebuild `tailored.roles` VERBATIM from the base resume
(titles/companies/dates/existing bullets byte-identical) and add keywords ONLY via
Summary augmentation + `_append_keyword_bullets` (β€3 gated lines appended per
role). ALL four bullet-mutation sites β incl. `_maximize_external_coverage` (LIVE
in max-ATS mode) β are gated behind `non_destructive`, so the candidate's real
history is never altered. LaTeX `inject_keywords` APPENDS β€3 `\item`s (tagged
`% ats-item`) after each experience instead of editing existing items. Proven by
`verify_non_destructive.py` (end-to-end `_generate_resume_v4` + `_maximum_ats_mode`
diff against the base). Coverage relocates to appended bullets+Skills+Summary
(still ~96%); the max-ATS regression threshold is an honest present-in-export floor.
- **Persistent side panel (R18):** `content.js::injectSidePanel` docks a persistent,
collapsible/dismissible left iframe hosting `popup/popup.html` (exposed via
`web_accessible_resources`), idempotent + non-blocking; the toolbar popup is a thin
launcher (`TOGGLE_PANEL`). The disappearing-popup "run restarted" feeling is gone;
the panel live-reflects the background-owned run.
- **History (R19):** `renderHistory` lists prior generations per job (title/company/
time/internal+external scores/status) with restore (via `applyResult`) +
re-download; `background.js` `MAX_SAVED` 10β40 with a `BYTE_BUDGET` quota guard
(`approxSize`/`stripOldestArtifacts`) that degrades oldest entries to
metadata-only, always keeping the newest fully downloadable.
Regressions: `verify_pdf_download.py`, `verify_non_destructive.py`,
`verify_side_panel.py`, `verify_history.py` (+ all Phase 7 scripts still pass).
**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_skills` render 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_keywords` keeps only
recognized skills (no "any noun"); the v4 LLM also returns a `jd_skills` array
(Jobalytics-style) that is unioned in. No prose-noun garbage.
- **Provider-independent.** Generation no longer assumes one model β see Β§6.
- The `_assert_no_dump_footer` postcondition 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(...)` β runs `regenerate_from_jobalytics` (full
v4 pipeline in `jobalytics_repair` mode with pasted addable keywords merged
into the include pool), then derives added / review-flag / unresolved-high /
blocked lists and final status (may upgrade to `READY_95_EXTERNAL_ALIGNED`).
Verified by `scripts/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_response` against
`JD_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.py` loads provider-specific templates from `prompts/`
(`{task}_{claude|kimi|nvidia}.txt`). `LLMClient.tailor_resume_v4(...,
provider_family=...)` selects the prompt. **Any stub/test that monkeypatches
`tailor_resume_v4` must accept `**kwargs`** (it takes `provider_family`).
- Pipeline records `provider_used` + `provider_response_quality` on 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_latex_resume.py` | LaTeX path: text extraction, honest keyword injection (22%β88% coverage), DISTRIBUTED placement (summary + experience + 1 competencies line), no fabrication (cissp/pmp/12+ years/cuda blocked), idempotent injection. |
| `scripts/verify_clean_jd_extraction.py` | R13: page/extension UI chrome (Simplify/Jobalytics CTA text) never becomes a keyword; genuine craft terms survive; content.js strips chrome. |
| `scripts/verify_skills_distribution.py` | R14: β€1 line per skills category (no repeated "Core Competencies:" dump), 18-28 total de-duplicated items. |
| `scripts/verify_resilient_run.py` | R15: background owns generation (runningβdone/error keyed by URL); popup restores + live-refreshes; popupβbackground storage contract matches. |
| `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_server` strips `content-encoding`/`content-length` when
proxying Streamlit (httpx already decompresses) β otherwise pages render blank.
- **Deploy gotcha:** pushing to `hf` rebuilds the Space (~2-15 min for Docker) and
causes transient 404/405s while the container is down. **Prefer pushing
extension-only changes to `origin` only** (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 ui` fails** 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_URL` points to
an externally hosted sidecar; known/non-blocking; dedicated scrapers still work.
- **Scrapling anti-block fetch layer** (`src/scrapers/fetch.py`): all scraper HTTP
goes through Scrapling (Chrome TLS impersonation via `Fetcher`; Cloudflare
bypass via `StealthyFetcher`/Camoufox) with a `requests` fallback. Set
`SCRAPER_PROXIES` (comma/newline list) to rotate proxies β the ONLY fix for
datacenter-IP blocking (fingerprint stealth β IP reputation). Camoufox is
installed via `scrapling install` in the Dockerfile (non-fatal).
- **Direct Company ATS** (`src/scrapers/company_ats.py`, platform key
`company_ats`, default-on): Greenhouse/Lever/Ashby public JSON boards β the most
block-resistant bulk source on HF. Extend via `COMPANY_ATS_BOARDS` env (JSON).
- **Streamlit batch persistence:** `hf_storage.py` syncs run history/resumes/CSV to
a private HF Dataset so data survives Space container restarts (needs
`HF_DATASET_REPO` + write token in Space secrets).
- **Security:** do not store HF write tokens in `.git/config` remote URLs β use
`huggingface-cli login` or a credential helper instead.
### Open / next
- Run `evaluate_model_providers.py` + `run_20_job_validation.py` with real
Kimi/NVIDIA keys (user's environment) β the live provider validation.
- Integrate a no-block job source for HF batch scraping β DONE via Scrapling
fetch layer + Direct Company ATS (Greenhouse/Lever/Ashby). Next: add a
residential proxy (`SCRAPER_PROXIES`) to make LinkedIn/Indeed reliable on HF,
and run a live bulk test to confirm block rates dropped.
---
## 9. Conventions (follow these)
- **Update `HISTORY.md` + `README.md` on every change** (standing user rule).
- **Commit/push only when asked.** Conventional Commits (`fix(scope): β¦`) + a
`Co-Authored-By` trailer when AI-assisted. Single `main` branch, no rebases.
- **Push extension-only changes to `origin` only**; push pipeline/API/HF changes
to both `origin` and `hf`.
- **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), then `candidate_fit.py` (what got included), then the
`_generate_resume_v4` repair loop. Run `verify_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.py`
repair endpoint β extension `REPAIR` handler. Run `verify_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) and `candidate_fit.py`
(inclusion). Don't reintroduce "any noun" extraction or fake injection.
---
## 11. Quick commands
```bash
# 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.
|