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-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:
- 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).
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::extractGenericJDstrips a CLONE (stripChrome: nav/aside/header/footer/button/overlay +simplify/jobalytics/jobscan/teal/__extensionnodes) andscrubOverlayLinesdrops 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_skillsemits ONE capped line per category (_PER_CATEGORY_CAP=10,_SKILLS_TOTAL_CAP=28; no morePER_LINE=12repeated 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_keywordsdistributes terms across a Summary sentence + woven Experience\items + ONE competencies line;_remove_injected_blockstrips every fragment (idempotent). - Resilient Run (R15):
extension/background.jsowns generation and persistsrunningβdone/errortoats_resultskeyed by normalized URL; the popup restores spinner/result on open + live-refreshes viachrome.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(+ existingverify_latex_resume.py/verify_max_ats_coverage.pystill 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
.pdfsidecar so only DOCX downloaded.api_servernow callspdf_writer.docx_to_pdfon both the generate and repair DOCX paths (reportlab off-Windows) βpdf_b64always populated. The LaTeX path returns apdf_error(engine_missingvslatex_error+compile_log) and STILL returnstex_b64on compile failure (200, not 500);Dockerfilehas a hardRUN tectonic --versiongate. - Non-destructive tailoring is the DEFAULT (R17):
NON_DESTRUCTIVE_DEFAULT=True_apply_non_destructiverebuildtailored.rolesVERBATIM 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 behindnon_destructive, so the candidate's real history is never altered. LaTeXinject_keywordsAPPENDS β€3\items (tagged% ats-item) after each experience instead of editing existing items. Proven byverify_non_destructive.py(end-to-end_generate_resume_v4+_maximum_ats_modediff 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::injectSidePaneldocks a persistent, collapsible/dismissible left iframe hostingpopup/popup.html(exposed viaweb_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):
renderHistorylists prior generations per job (title/company/ time/internal+external scores/status) with restore (viaapplyResult) + re-download;background.jsMAX_SAVED10β40 with aBYTE_BUDGETquota 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_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_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_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. - Scrapling anti-block fetch layer (
src/scrapers/fetch.py): all scraper HTTP goes through Scrapling (Chrome TLS impersonation viaFetcher; Cloudflare bypass viaStealthyFetcher/Camoufox) with arequestsfallback. SetSCRAPER_PROXIES(comma/newline list) to rotate proxies β the ONLY fix for datacenter-IP blocking (fingerprint stealth β IP reputation). Camoufox is installed viascrapling installin the Dockerfile (non-fatal). - Direct Company ATS (
src/scrapers/company_ats.py, platform keycompany_ats, default-on): Greenhouse/Lever/Ashby public JSON boards β the most block-resistant bulk source on HF. Extend viaCOMPANY_ATS_BOARDSenv (JSON). - 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 β 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.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.