saitejatirunagari Claude Opus 4.7 (1M context) commited on
Commit
197c01c
·
1 Parent(s): 386446e

docs: add AGENT_CONTEXT.md — full project handoff doc for new agents/IDEs

Browse files

Comprehensive context document for new Claude/Cursor/agent sessions to pick up
where current work left off. Covers:
- Project mission + pipeline flow
- Complete file map with purpose of each module
- 3 design phases delivered (scale to 170+ platforms, HR-grade resume, ATS floor 90%+)
- 4 known bugs identified in production audit (table-header noise leak, orphan
continuation lines, duplicate dates, v1 schema fallback)
- How to verify changes (test scripts, postconditions, deployment)
- LLM model pool details
- 6 critical user-stated policies (no Skills section, no hidden text, etc.)
- Deployment details (GitHub + HF Spaces)
- Where to start for common task types (ATS fixes, format bugs, LLM behavior, UI)
- Prioritized punch list of what to do next
- Conventions, glossary, quick command reference

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files changed (1) hide show
  1. AGENT_CONTEXT.md +386 -0
AGENT_CONTEXT.md ADDED
@@ -0,0 +1,386 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Agent Context — Job Automation Agent
2
+
3
+ > **Read this first.** This document gives a new agent (Claude/Cursor/etc.) or developer everything they need to understand the project, its architecture, the design decisions that shaped it, what's working, what's broken, and where to start.
4
+
5
+ Last updated: 2026-06-16
6
+ Active branch: `main`
7
+ Last deployed commit: see `git log --oneline -1`
8
+
9
+ ---
10
+
11
+ ## 1. Project mission in one sentence
12
+
13
+ Upload a PM resume → the tool scrapes Product Manager jobs from 170+ platforms, scores each against the resume, tailors a per-job resume that targets ATS systems (≥90% match), and logs everything to Google Sheets.
14
+
15
+ Stack: Python 3.11 + Streamlit UI + NestJS sidecar (`ever-jobs`) + multi-LLM pool (GLM / Kimi / Step / Qwen) + python-docx + reportlab + Hugging Face Spaces (Docker).
16
+
17
+ Live: https://huggingface.co/spaces/saitejatirunagari/JAA-ATS-Tool
18
+ Repo: https://github.com/saitejatiru/JAA-ATS-Tool
19
+
20
+ ---
21
+
22
+ ## 2. End-to-end pipeline flow
23
+
24
+ ```
25
+ User uploads resume.pdf
26
+
27
+ [ui.py / Streamlit wizard]
28
+ - 7-step wizard collects: roles, locations, platforms, freshness, AI score, sheets
29
+ - "Start AI Job Search" triggers run_pipeline() in a thread
30
+
31
+ [Scrapers] — src/scrapers/*.py + ever-jobs sidecar
32
+ - Dedicated: linkedin / indeed / glassdoor / remotive / weworkremotely / naukri
33
+ - All other 160+ platforms: EverJobsScraper → calls NestJS sidecar at localhost:3001
34
+
35
+ [Deduplication] — URL + content-fingerprint
36
+
37
+ [Job assessment] — src/job_assessor.py + multi-LLM pool
38
+ - LLM extracts: relevance_score (1-10), matching_skills, missing_skills, ats_keywords
39
+
40
+ [Resume tailoring] — src/resume_customizer.py
41
+ - Per-job, parallel across LLM pool keys
42
+ - Calls llm_client.customize_resume_fast with INDEXED bullets (role:idx)
43
+ - LLM returns: professional_summary + rewritten_bullets + new_bullets
44
+ - DOCX written via python-docx; iteration loop scores and retries (up to 3x)
45
+ - Falls back to template + aggressive keyword injection if LLM weak
46
+
47
+ [ATS scoring] — src/ats_scorer.py
48
+ - Extracts JD keywords (regex + LLM-extracted from assessment)
49
+ - Lemma + phrase matching against resume text
50
+ - Returns: ats_score, jd_match_score, resume_quality, matched_kw, missing_kw, penalties
51
+
52
+ [PDF generation] — src/pdf_writer.py
53
+ - Windows + Word installed → docx2pdf (perfect fidelity, dev only)
54
+ - HF Spaces / Linux → reportlab (replicates DOCX in document order)
55
+
56
+ [Google Sheets logging] — src/gsheets.py
57
+ - One row per job with ATS Before/After, resume path, JD URL, status
58
+
59
+ [Run history] — data/output/run_history/<timestamp>.json
60
+ - Snapshot of every run for re-display in UI
61
+ ```
62
+
63
+ ---
64
+
65
+ ## 3. File map (what each file does)
66
+
67
+ ### Entry points
68
+ | File | Purpose |
69
+ |---|---|
70
+ | `ui.py` | Streamlit app — the only user-facing entry point. ~2300 lines. Contains all UI, CSS, wizard, results display, history panel, and orchestration of `run_pipeline()`. |
71
+ | `main.py` | CLI entry (not user-facing on HF). Useful for debugging headless. |
72
+ | `start.sh` | Boots NestJS sidecar (ever-jobs) on :3001, then Streamlit on :7860. |
73
+ | `Dockerfile` | HF Spaces build: Python 3.11-slim + Node 20 + Playwright Chromium + ever-jobs clone. |
74
+
75
+ ### Core pipeline (`src/`)
76
+ | File | Purpose |
77
+ |---|---|
78
+ | `resume_parser.py` | Extracts text + contact info from PDF resume via pdfplumber. |
79
+ | `job_assessor.py` | LLM-driven relevance scoring per job. Returns dict with score / matching / missing / keywords. |
80
+ | `resume_customizer.py` | **The heart of the tool.** DOCX generation, LLM-tailoring loop, keyword injection, postcondition checks. ~1100 lines. |
81
+ | `llm_client.py` | Multi-model LLM client. Supports GLM / Kimi / Step / Qwen via OpenAI-compatible APIs. Handles JSON extraction, retries, parallel calls. |
82
+ | `model_pool.py` | Round-robin model selection across configured keys. |
83
+ | `ats_scorer.py` | Lemma + phrase-aware keyword matching. Scoring formula: JD-match 70% + quality 30%. |
84
+ | `pdf_writer.py` | DOCX → PDF (docx2pdf on Windows, reportlab fallback elsewhere). |
85
+ | `gsheets.py` | Google Sheets append. Service account or OAuth. |
86
+ | `excel_reporter.py` | XLSX export of results. |
87
+ | `run_history.py` | Save/load past run snapshots. |
88
+ | `job_history.py` | SQLite store for "already-applied" deduplication. |
89
+ | `app_logger.py` | Color-formatted logging. |
90
+
91
+ ### Scrapers (`src/scrapers/`)
92
+ | File | Purpose |
93
+ |---|---|
94
+ | `base.py` | Abstract Scraper class, common Job dataclass. |
95
+ | `linkedin.py` / `indeed.py` / `glassdoor.py` / `remotive.py` / `weworkremotely.py` / `naukri.py` | Dedicated scrapers with hand-tuned selectors. |
96
+ | `ever_jobs.py` | Adapter that calls the NestJS sidecar for 160+ other platforms. |
97
+
98
+ ### Ever-jobs integration (`src/ever_jobs_bridge/`)
99
+ | File | Purpose |
100
+ |---|---|
101
+ | `server.py` | Lifecycle management of the NestJS sidecar process. |
102
+ | `client.py` | HTTP client for the sidecar (host: localhost:3001). |
103
+ | `mapper.py` | Maps ever-jobs `IJob` shape → our `Job` dataclass. |
104
+ | `platforms.py` | Registry of all 170+ platforms grouped into Search Boards / ATS Platforms / Company Pages. |
105
+
106
+ ### Research (deprecated, kept for reference)
107
+ | File | Purpose |
108
+ |---|---|
109
+ | `research/deep_researcher.py` etc. | Earlier experiment — JD-driven deep research before assessment. Not in the production path. |
110
+
111
+ ### Config
112
+ | File | Purpose |
113
+ |---|---|
114
+ | `config.py` | Loads `.env`, builds MODELS list, GOOGLE config dict. |
115
+ | `.env` | API keys for LLMs (GLM/Kimi/Step/Qwen) + Google Sheets ID. **Never committed.** |
116
+ | `google_credentials.json` / `google_oauth_client.json` | Sheets auth files. **Never committed.** |
117
+
118
+ ### Planning (gsd workflow artifacts)
119
+ | Directory | Purpose |
120
+ |---|---|
121
+ | `.planning/ROADMAP.md` | Milestone + phases. |
122
+ | `.planning/REQUIREMENTS.md` | R1-R10 numbered requirements. |
123
+ | `.planning/STATE.md` | Project-level decisions. |
124
+ | `.planning/phases/<NN>-<slug>/<NN>-<MM>-PLAN.md` | Per-plan execution specs. |
125
+
126
+ ### Tests + scripts
127
+ | File | Purpose |
128
+ |---|---|
129
+ | `tests/fixtures/jds/*.txt` | Real production JDs used for verification (airtel, sumo_logic, edgeverve, aditya_birla, aisensy, navi, zenda, generic). |
130
+ | `scripts/verify_phase3.py` | Validate 2 hardest JDs hit ≥90%. |
131
+ | `scripts/verify_phase3_all4.py` | Validate all 4 failing JDs hit ≥90%. |
132
+ | `scripts/verify_weak_llm_recovery.py` | Validate weak-LLM + injection still recovers to 87-91%. |
133
+
134
+ ### Output (gitignored)
135
+ | Path | Purpose |
136
+ |---|---|
137
+ | `data/resume/resume.pdf` | User's uploaded resume. |
138
+ | `data/output/resumes/YYYY-MM-DD/<Company>_<Role>.docx` and `.pdf` | Generated resumes. |
139
+ | `data/output/reports/*.xlsx` | Excel summary per run. |
140
+ | `data/output/run_history/*.json` | Run snapshots for history panel. |
141
+ | `data/logs/tailoring_YYYY-MM-DD.jsonl` | Per-job diagnostic log (added Phase 3). |
142
+ | `data/research_cache/` | LLM response cache to save tokens. |
143
+
144
+ ---
145
+
146
+ ## 4. The 3 design phases delivered
147
+
148
+ ### Phase 1 — Scale to 170+ platforms (committed)
149
+ - Integrated `github.com/ever-jobs/ever-jobs` as a NestJS sidecar
150
+ - Adapter pattern in `EverJobsScraper`
151
+ - Cross-platform dedup via URL + content fingerprint
152
+ - Streamlit UI grouped multiselect with 170 platforms across 3 categories
153
+ - ATS scoring confirmed adequate (R3 closed — ever-jobs has no resume scorer)
154
+
155
+ ### Phase 2 — HR-grade resume + bullet-rewriter contract (committed)
156
+ **Trigger**: User saw a 1-page truncated resume with header "Internal Product" (broken name extraction), missing BYJU's/ML Edutech roles, no Education, empty Core Competencies, plus a spam "Additional relevant skills: adani • godrej • yakult" footer.
157
+
158
+ **Fixes**:
159
+ - New `_extract_candidate_name()` handles ALL CAPS names and PDF letter-spacing
160
+ - Experience parser walks the whole experience blob, finds all date ranges (including line-wrapped "Oct 2021 – Dec\n2022"), splits into roles, preserves sub-section headers as bold §§HEADER§§ markers
161
+ - LLM contract v2: returns `rewritten_bullets["0:3"]` keyed by `role:idx` instead of generic highlights block
162
+ - `_inject_missing_keywords` no longer appends a footer — weaves missing skills into a closing sentence of the Professional Summary
163
+ - Postcondition `_assert_no_dump_footer` raises if any banned section header or "Additional relevant skills" line slips through
164
+ - **User explicitly directed: NO CORE COMPETENCIES section** — keywords live only in summary + bullets
165
+
166
+ **Net**: Original 57/100 → Tailored 92/100 on AiSensy JD (verified with handcrafted LLM response).
167
+
168
+ ### Phase 3 — ATS floor 90%+ on real LLM runs (committed)
169
+ **Trigger**: Real LLM in production averaged ~60% (airtel 79, Aditya Birla 48, EdgeVerve 63, Sumo Logic 52) — Phase 2 v2 contract worked but real LLM rewrites covered fewer keywords than handcrafted tests.
170
+
171
+ **Fixes**:
172
+ - Rules-based lemmatizer (no NLTK): `automated`↔`automation`, `roadmaps`↔`roadmap`
173
+ - Phrase-aware matching: multi-word JD keywords match exact OR all-lemmas-in-5-token-window
174
+ - Aggressive JD noise filter: drops 30+ categories (adjectives like proven/solid, modals like will/must, process verbs like perform/establish, JD section words like what/doing/inc/bachelor)
175
+ - LLM prompt updated with **liberal-keyword policy** (user authorized): assume candidate has touched any JD-named common tool over 5+ years
176
+ - **Recruiter-pitch opener**: every Professional Summary opens with `"Strong-fit candidate for <role> at <company>: ..."` — visible to humans + AI screeners (the safe alternative to the user's hidden-text request, which modern ATS auto-rejects)
177
+ - Aggressive keyword injection: trusts JD extractor's filter, drops only lemmatizer artifacts and short tokens
178
+ - Diagnostic JSONL log: per-job record of JD keywords / matched / missing / LLM schema / pitch detection — for debugging future score regressions
179
+ - UI: removed "🤖 Job Automation Agent" title; added baseweb CSS overrides for dark-on-dark dropdowns
180
+
181
+ **Verified results** (handcrafted v3 simulations against the 4 failing JDs):
182
+ - Airtel: 79 �� **92** (+13pp)
183
+ - EdgeVerve: 63 → **91** (+28pp)
184
+ - Sumo Logic: 52 → **92** (+40pp)
185
+ - Aditya Birla: 48 → **91** (+43pp)
186
+
187
+ ---
188
+
189
+ ## 5. Known issues — what's still wrong
190
+
191
+ After the user audited a real production output (Aditya Birla LLM-tailored resume, reported 95% by our scorer), three concrete bugs were identified that drop the **honest** ATS score to ~85%:
192
+
193
+ ### Bug A: JD table-header words leak into keyword injection
194
+ The Aditya Birla JD has tabular formatting like `KRA (Accountabilities) (Max 1325 Characters)` and `Supporting Actions`. The keyword extractor pulls **Accountabilities, Max, Characters, Actions, Show** as proper nouns. The aggressive injection then weaves them into the Summary as if they were skills:
195
+
196
+ > *"Toolchain and domain coverage includes Accountabilities, Max, Characters, Actions, and Show."*
197
+
198
+ This is obviously garbage and would make a recruiter think the resume is AI-spam.
199
+
200
+ **Fix needed** in `src/ats_scorer.py` `_JD_NOISE_WORDS`: add table-cell boilerplate (Accountabilities, Max, Characters, Show, Supporting, KRA, KRAs, and similar tabular-formatting terms).
201
+
202
+ ### Bug B: Multi-line bullets leave orphan continuation lines
203
+ When the original PDF resume has a multi-line bullet (text wraps in the source), `pdfplumber` extracts each line as a separate text block. `_extract_experience_sections` treats each as a separate bullet → indexes them as `0:0`, `0:1`, `0:2`. The LLM rewrites `0:0` but the orphan continuation (the rest of the original bullet) renders as a ghost-bullet.
204
+
205
+ Example from the user's audit:
206
+ - Bullet 0:0 rewritten: *"Owned end-to-end revamp of NIAT Application Portal—a unified digital IT project covering landing pages → OTP login → personal details → payment, delivering on approved scope, time, and cost."*
207
+ - Orphan that follows: *"details → payment → slot booking → exam → report → sales flow—integrated with CRM, WebEngage, and payment systems."*
208
+
209
+ **Fix needed** in `src/resume_customizer.py` `_extract_experience_sections`: detect line wraps (line ends mid-sentence without punctuation, no bullet character) and join continuation lines into the previous bullet's text before indexing.
210
+
211
+ ### Bug C: Date wrapping creates duplicate dates
212
+ `Think & Learn Pvt. Ltd. (BYJU'S) | Bengaluru, IndiaOct 2021 – Dec | Oct 2021 – Dec 2022` — the date appears twice because the original PDF had the date wrapping across lines, and the parser captures both the partial first line and the full second line.
213
+
214
+ **Fix needed**: collapse whitespace and detect duplicate date strings in `_extract_experience_sections`.
215
+
216
+ ### Bug D: LLM sometimes returns v1 schema in production
217
+ The new v2 contract asks for `rewritten_bullets["0:3"]` but smaller models (Step / Qwen smaller variants) sometimes return the old v1 `experience_bullets` shape. Backward-compat handles it but the result is generic bullets that don't use JD-specific phrasing.
218
+
219
+ **Fix needed**: stricter JSON-schema enforcement in `customize_resume_fast` (retry if v2 fields are missing); OR add a converter that maps v1 → v2 by best-effort matching of rewritten bullets to original bullets via fuzzy substring match.
220
+
221
+ ---
222
+
223
+ ## 6. How to verify changes
224
+
225
+ | Goal | How |
226
+ |---|---|
227
+ | Test scorer changes alone | `PYTHONPATH=. python scripts/verify_phase3.py` (uses handcrafted LLM v3 sims) |
228
+ | Test all 4 failing JDs | `PYTHONPATH=. python scripts/verify_phase3_all4.py` |
229
+ | Test injection works on weak LLM output | `PYTHONPATH=. python scripts/verify_weak_llm_recovery.py` |
230
+ | Test UI locally | `streamlit run ui.py --server.port 8502` |
231
+ | Read tailoring diagnostic log | `cat data/logs/tailoring_$(date +%Y-%m-%d).jsonl | jq` |
232
+ | Verify the no-Skills-section postcondition | All generated DOCX files run through `_assert_no_dump_footer` — raises on banned sections |
233
+ | Push to HF Spaces | `git push hf main` (auto-rebuilds in ~5 min) |
234
+
235
+ ---
236
+
237
+ ## 7. LLM model pool
238
+
239
+ Configured via `.env`:
240
+ ```
241
+ GLM_KEY_1=... ← Primary (GLM-4.6) — best JSON adherence
242
+ KIMI_KEY_1=... ← Kimi K2 — fast, good for parallel calls
243
+ STEP_KEY_1=... ← Step-1 — fallback
244
+ QWEN_KEY_1=... ← Qwen — fallback
245
+ ```
246
+
247
+ `model_pool.py` round-robins across configured keys. Calls run in parallel via `ThreadPoolExecutor` (up to 6 workers).
248
+
249
+ Two LLM call sites:
250
+ 1. `LLMClient.assess_job()` — fast model, JSON output: `{relevance_score, matching, missing, ats_keywords}`
251
+ 2. `LLMClient.customize_resume_fast()` — fast model, JSON output (v2 contract): `{professional_summary, rewritten_bullets, new_bullets, key_achievements}`
252
+
253
+ If JSON parse fails, retries up to 2 times per call. If all retry, `_empty_customization()` returns empty schema and the pipeline falls through to template + injection.
254
+
255
+ ---
256
+
257
+ ## 8. Critical user-stated policies (do not change without asking)
258
+
259
+ 1. **No CORE COMPETENCIES / Skills section anywhere in the tailored resume.** Keywords MUST live in Summary + experience bullets only. The `_assert_no_dump_footer` postcondition enforces this.
260
+
261
+ 2. **Liberal keyword inclusion is authorized.** When a JD names a common PM tool (Jira/Figma/Mixpanel/etc.) or methodology (PRDs/sprint/MLOps), the LLM is told to include it assuming the candidate has touched it. The user owns interview-side risk.
262
+
263
+ 3. **No hidden text / prompt injection.** User asked for white-on-white prompts targeting AI screeners — refused because modern ATS detects this pattern and auto-rejects + blacklists. Replaced with the visible recruiter-pitch opener.
264
+
265
+ 4. **No "Tailored for X at Y" footer or banner.** Removed in Phase 2.
266
+
267
+ 5. **All 4 candidate roles must be preserved** (NxtWave + BYJU's-1 + BYJU's-2 + ML Edutech). The experience parser must handle all dated headers.
268
+
269
+ 6. **Output is BOTH .docx AND .pdf.** PDF via docx2pdf when on Windows + Word; reportlab everywhere else (HF Spaces). PDFs must include table content via document-order body iteration.
270
+
271
+ ---
272
+
273
+ ## 9. Deployment + branches
274
+
275
+ - **GitHub**: https://github.com/saitejatiru/JAA-ATS-Tool (branch: `main`)
276
+ - **HF Spaces**: https://huggingface.co/spaces/saitejatirunagari/JAA-ATS-Tool (auto-builds on `git push hf main`)
277
+ - Single `main` branch; no PR workflow. Direct commits to main, pushed to both remotes.
278
+ - HF rebuild takes ~5-7 min. The build log shows the commit SHA at the top.
279
+
280
+ ---
281
+
282
+ ## 10. Where to start as a new agent
283
+
284
+ If your task is **fixing low ATS scores**:
285
+ 1. Read `src/ats_scorer.py` `score_resume()` and `extract_jd_keywords()`
286
+ 2. Read `src/resume_customizer.py` `_generate_resume()` (the iteration loop) and `_inject_missing_keywords()`
287
+ 3. Check `data/logs/tailoring_*.jsonl` for what the LLM actually returned
288
+ 4. Test changes with `scripts/verify_phase3_all4.py`
289
+
290
+ If your task is **fixing format / layout bugs**:
291
+ 1. Read `src/resume_customizer.py` `_write_docx()` and `_extract_experience_sections()`
292
+ 2. Read `src/pdf_writer.py` `_reportlab_render()`
293
+ 3. Generate a test DOCX locally: see `scripts/verify_phase3.py` for the pattern
294
+ 4. Inspect with python-docx or open in Word/Preview
295
+
296
+ If your task is **adding new LLM behavior**:
297
+ 1. Read `src/llm_client.py` `_resume_customize_prompt()` — the v2 contract prompt
298
+ 2. Update prompt and schema validator (`_customization_valid()`)
299
+ 3. Verify backward compatibility — older models may return v1 schema
300
+
301
+ If your task is **UI changes**:
302
+ 1. `ui.py` is ~2300 lines — search for the relevant section by keyword
303
+ 2. CSS is at the top (~lines 30-200); be careful with `[data-baseweb=...]` overrides
304
+ 3. The 7-step wizard logic uses `st.session_state.setup_step` and persistent `_cfg_*` keys
305
+
306
+ ---
307
+
308
+ ## 11. The "what to do next" punch list
309
+
310
+ In priority order:
311
+
312
+ 1. **Fix Bug A** (JD table-header words leaking into injection) — add `Accountabilities`, `Max`, `Characters`, `Actions`, `Show`, `Supporting`, `KRA`, `KRAs` to `_JD_NOISE_WORDS` in `src/ats_scorer.py`. ~5 min.
313
+
314
+ 2. **Fix Bug B** (orphan continuation lines from multi-line bullets) — in `src/resume_customizer.py` `_extract_experience_sections`, before indexing bullets, join consecutive non-bullet lines into the previous bullet's text. ~30 min, test against the production Aditya Birla DOCX.
315
+
316
+ 3. **Fix Bug C** (duplicate date string) — collapse whitespace and dedupe in the role header parser. ~10 min.
317
+
318
+ 4. **Address Bug D** (v1 schema fallback hurts quality) — either enforce v2 schema with stricter retry, or add a v1→v2 converter that fuzzy-matches each "highlight" bullet to its closest original bullet via SequenceMatcher and rewrites the original in place. ~1-2 hr.
319
+
320
+ 5. **Verify** by re-running on the same 4 JDs in production, then asking the user to share the new docx for honest audit.
321
+
322
+ ---
323
+
324
+ ## 12. Conventions you should follow
325
+
326
+ - **Read HISTORY.md** for the chronological log of fixes — gives context on WHY decisions were made.
327
+ - **Read .planning/REQUIREMENTS.md** — R1-R10 are the named requirements.
328
+ - **Don't change git history** — single `main` branch, no rebases.
329
+ - **Always test against the real resume PDF** at `C:\Users\Nxtwave\Desktop\resume\Saiteja_Tirunagari_Resume A 26 - Copy.pdf` (Saiteja's actual resume) — that's the gold reference.
330
+ - **Commit messages**: Conventional Commits style (`fix(scope): ...`, `feat(scope): ...`), with a Co-Authored-By trailer if AI-assisted.
331
+ - **No emojis in code or commit messages** unless the user explicitly asks for them.
332
+ - **No hidden text / prompt injection in resumes** — see policy #3.
333
+ - **The user wants honesty over false confidence.** If a fix only addresses part of the problem, say so. If our scorer reports 95% but real ATS would score 85%, say so.
334
+
335
+ ---
336
+
337
+ ## 13. Quick command reference
338
+
339
+ ```bash
340
+ # Run UI locally
341
+ streamlit run ui.py --server.port 8502
342
+
343
+ # Verify Phase 3 against 2 hardest JDs
344
+ PYTHONPATH=. python scripts/verify_phase3.py
345
+
346
+ # Verify all 4 failing JDs
347
+ PYTHONPATH=. python scripts/verify_phase3_all4.py
348
+
349
+ # Generate a test resume (handcrafted v3 LLM sim)
350
+ PYTHONPATH=. python scripts/verify_weak_llm_recovery.py
351
+
352
+ # Deploy to GitHub + HF Spaces
353
+ git add -A
354
+ git commit -m "fix(scope): short description"
355
+ git push origin main
356
+ git push hf main
357
+
358
+ # Check HF build status
359
+ # → Open https://huggingface.co/spaces/saitejatirunagari/JAA-ATS-Tool and check the Logs tab
360
+
361
+ # Tail tailoring diagnostic log (after a run)
362
+ type "data\logs\tailoring_2026-06-16.jsonl" # Windows
363
+ cat data/logs/tailoring_2026-06-16.jsonl # Linux/Mac/git-bash
364
+ ```
365
+
366
+ ---
367
+
368
+ ## 14. Glossary of project-specific terms
369
+
370
+ | Term | Meaning |
371
+ |---|---|
372
+ | **v1 contract** | Original LLM output schema: `{summary, core_competencies, experience_bullets, key_achievements}`. Deprecated but still supported via backward compat. |
373
+ | **v2 contract** | Current LLM output schema (Phase 2): `{summary, rewritten_bullets, new_bullets, key_achievements}`. Bullets keyed by `role_idx:bullet_idx`. |
374
+ | **v3 contract** | Phase 3 prompt with liberal keyword policy + recruiter-pitch opener requirement. Same schema as v2. |
375
+ | **Recruiter pitch** | First sentence of Professional Summary, format: *"Strong-fit candidate for <role> at <company>: <N> years of <relevant experience> directly applicable to <3 JD requirements>."* |
376
+ | **Indexed bullets** | `[(role_idx, bullet_idx, role_name, bullet_text), ...]` tuples passed to the LLM so it can reference specific original bullets. |
377
+ | **§§HEADER§§** / **§§META§§** | Internal markers in `_extract_experience_sections` for sub-section bold headers and italic meta lines (like "Scope:"). |
378
+ | **JD noise filter** | Set of words `_JD_NOISE_WORDS` in `ats_scorer.py` that get dropped from keyword extraction (e.g. proven/solid/will/bachelor). |
379
+ | **Postcondition** | `_assert_no_dump_footer` runs at end of every `_generate_resume` to catch banned patterns (Skills sections, dump footers). |
380
+ | **Lemma matching** | Rules-based stemmer: `automated`/`automation`/`automate` all collapse to `automat`. Enables forgiving keyword matching. |
381
+ | **Phrase matching** | Multi-word JD keywords match if all component lemmas appear within a 5-token sliding window in the resume. |
382
+ | **Aggressive injection** | Trusts JD extractor's noise filter — injects every still-missing keyword (capped at 15) into the Summary's closing sentence. |
383
+
384
+ ---
385
+
386
+ End of context document. If anything in here contradicts current code, the code is the source of truth — but please update this doc when you finish your task.