JAA-ATS-Tool / CLAUDE.md
saitejatirunagari's picture
feat: competency-aware keyword placement, Skills-first design (v2.6.0)
8d38bb5
|
Raw
History Blame Contribute Delete
8.1 kB

Job Automation Agent

ATS resume tailoring system: Chrome extension + HuggingFace server pipeline. Extracts JDs from job boards, generates tailored resumes via LLM pool, returns compiled PDFs.

Scope Lock

Do NOT redesign the ATS extractor, scorer, evidence gate, or rewriting architecture. Do NOT work on V2 (multi-agent natural tailoring). Current work is V3 bulk generation.

Security

  • Never hard-code API tokens into committed JS/manifest/packaged assets
  • Never expose server filesystem paths to the client
  • Sanitize compiler output before returning it
  • Disable shell escape in LaTeX, use unique temp dirs, apply strict compilation timeouts

Extension Architecture (MV3)

Files

File Role
extension/manifest.json MV3 manifest, <all_urls> content script, side panel
extension/content.js JD extraction + job card scanning, runs on every page
extension/popup/popup.js Side panel UI: single gen, bulk gen, results, editors
extension/popup/popup.html Side panel markup (380px popup, 100% sidepanel)
extension/background.js Service worker: generation lifecycle, SSE streaming, storage persistence
extension/options/options.html Settings: API URL/token, resume LaTeX, gen version, autofill profile
extension/default_resume.js Fallback LaTeX resume (self.DEFAULT_RESUME_LATEX)

Content Script Message Types

Message Handler Async
EXTRACT_JD extractJD() platform router no
SCAN_JOB_CARDS scanJobCards() / scanJobCardsLinkedIn() no
NAVIGATE_EXTRACT_JOB navigateAndExtractJD(jobId) — LinkedIn pushState yes (return true)
FETCH_EXTRACT_JOB fetchExtractJD(url) — background fetch+parse yes
CLICK_AND_EXTRACT_JOB clickAndExtractJob(index) — click card fallback yes
SCAN_FORM scanApplicationFields() no
APPLY_FORM_FILL applyAutofillAnswers(answers) no

LinkedIn Bulk Extraction (V3)

LinkedIn uses SDUI (Server-Driven UI) with virtualized rendering:

  • li[data-occludable-job-id] elements exist for all 25 jobs but only ~7 visible ones have content
  • Programmatic scroll does NOT trigger hydration
  • Solution: scanJobCardsLinkedIn() reads job IDs from ALL <li> elements (even empty placeholders). navigateAndExtractJD(jobId) uses history.pushState to set currentJobId in the URL + dispatchEvent(popstate) to trigger LinkedIn's SPA router. Detail pane updates with full JD.
  • No clicking, no navigation, no profile redirects.
  • #job-details element contains the full JD text in the detail pane.
  • expandDetailPane() clicks only <button> "show more" elements (never <a> links — those navigate to company pages).

Bulk Generation Flow (popup.js)

  1. checkBulkAvailable() runs on init — requires genVersionDefault === 'v3' AND URL matches BULK_HOSTS
  2. Sends SCAN_JOB_CARDS to content script; shows button if jobs.length > 1
  3. handleBulkGenerate() processes jobs sequentially:
    • LinkedIn (has job_id): NAVIGATE_EXTRACT_JOB
    • Others: FETCH_EXTRACT_JOB then CLICK_AND_EXTRACT_JOB fallback
  4. Each extracted JD → generateForBulkJob() → SSE stream → result saved
  5. Bulk history persisted in ats_bulk_history (max 5 runs, LaTeX/PDF stripped for quota)

Platform Support

Platform Scan Extract Method
LinkedIn data-occludable-job-id pushState + #job-details SPA router
Indeed CSS card selectors #jobDescriptionText fetch or click
Naukri CSS card selectors .job-desc fetch or click
Glassdoor CSS card selectors [data-test="job-details-container"] fetch
IIMJobs CSS card selectors generic fetch
Generic text-density heuristic densest block direct

Server Endpoints

Endpoint Method Purpose
/api/health GET Connection check
/api/generate-application-stream POST (multipart) SSE resume generation
/api/generate-stream POST SSE generation (background.js)
/api/generate POST Blocking fallback
/api/compile-resume POST LaTeX recompile
/api/compile-cover-letter POST CL LaTeX recompile
/api/repair-with-feedback POST Feedback-based repair
/api/form-assist POST AI form field answers

Storage Keys

Key Purpose
api_url, api_token Server connection
resume_latex, resume_b64 User's resume (LaTeX preferred, PDF fallback)
default_cover_letter_latex CL template
gen_version_default v3/v2/v1 — controls bulk visibility
ats_app_results Per-URL result cache (max 10)
ats_bulk_history Bulk run history (max 5)
autofill_profile_json 16-field profile for form autofill

Scoring & Honesty Contract (V1)

The score is a percentage of what the JD actually asks for. Invariants — breaking any of these is how the pipeline previously reported a fabricated 90–100:

  1. Denominators include gaps. Coverage is present_in_final / ALL_JD_criteria, never / supported_subset. Dividing by the supported subset made "2 of 26 must-haves" = 100%.
  2. No free points. A category the JD does not mention is dropped and its weight redistributed — never scored 1.0. Never default a missing component to full marks.
  3. Criticality comes from the JD, not from our calibration_weight. Calibration up-ranks what the résumé already evidences, so scoring off it is self-grading.
  4. Tool gaps are never written; competency gaps go to Skills. Claiming a specific tool/platform the resume never mentions IS fabrication (the old "LinkedIn Ads" bug). But soft skills, responsibilities, domain knowledge, and core competencies are placed into the Skills section — a senior PM/founder has these skills even if the resume doesn't list every one. Years, degrees, and certifications stay blocked.
  5. Never re-calibrate on post-placement text. An inserted keyword is not evidence. Calibration stays anchored to the ORIGINAL résumé.
  6. Truthful ≠ readable. verify_rewrite runs _reads_naturally(); a rewrite whose tokens are all legitimate can still be nonsense ("communication skills cadence").
  7. Never round-trip LaTeX through plain text. Use _surgical_latex_edit(); re-escaping stripped text turns \textrightarrow into \textbackslash{} and loses \textbf{}.
  8. Partial evidence needs same-statement co-occurrence. Whole-document token matching combined linkedin.com (contact line) + "Ads" into a fabricated "LinkedIn Ads".

With competency gap placement, an honest score lands in the 65–85 band. Remaining gaps are tool/platform-specific skills and hard constraints (years, degrees). The ceiling reflects the max reachable score; score == ceiling means the résumé is maxed out.

Keyword extraction must yield searchable noun phrases. _is_ats_keyword() rejects by shape (parentheticals, clause markers, pronouns, weak/verb heads, dangling function words), never by topic, so unfamiliar real skills still pass.

Development

Build Extension Zip

Compress-Archive -Path "extension\*" -DestinationPath "resume-tailor-extension-v{VERSION}.zip" -Force

Version Bump Checklist

  1. extension/manifest.json — bump version
  2. HISTORY.md — add entry at top
  3. Build zip with new version number

Key Gotchas

  • LinkedIn SDUI: CSS classes are hashed/random, never rely on them. Use data-* attributes and semantic selectors.
  • expandDetailPane() must NEVER click <a> tags — LinkedIn "Show more" links navigate to company pages.
  • Content script double-injection guard: window.__atsContentScriptLoaded prevents duplicate listeners.
  • Service worker keepalive: startKeepAlive() pings every 20s during long SSE fetches to prevent MV3 suspension.
  • fetchExtractJD() returns SPA shell for LinkedIn (not full JD) — that's why LinkedIn uses pushState instead.