# Project History — Job Automation Agent A running log of everything built, fixed, and changed. Most recent first. --- ## 2026-09-02 — Fix placement + LaTeX compile failures (v2.6.1) Two critical bugs from v2.6.0 fixed: - **"placed 0 keywords" bug.** Three filters conspired to empty the placement list: (1) `key in text_low` was Python substring search — "strategy" matched inside "product strategy," so every keyword that appeared as part of a longer phrase was silently killed. Replaced with word-boundary regex matching. (2) The prepositional fragment filter ran BEFORE leading-verb stripping, so "experience in talent management" was killed before "experience" could be stripped to leave "Talent Management." Reordered: verb strip first, then prepositional check. (3) `placeable` in ats_safe.py included already-supported keywords alongside competency gaps — they went into `place_keywords_naturally` only to be filtered (already in text). Now only unsupported competency gaps are sent. - **LaTeX compile failure on every resume.** `\usepackage{carlito}` and `\usepackage{tfrupee}` are not in Tectonic's bundle. Every resume compiled to `Undefined control sequence` at `\rupmark`. The sanitizer now replaces carlito with helvet (always available) and tfrupee with `\newcommand{\rupmark}{Rs.}`. The `.tex` download retains the original packages for Overleaf. --- ## 2026-09-02 — Competency-aware placement: place skills you have but haven't listed (v2.6.0) The pipeline was treating "not mentioned in your resume" as "you don't have it." A senior PM/founder with 11 years of experience has stakeholder management, cross-functional collaboration, data-driven decisions, etc. — even if their resume doesn't spell out every one. Fixed the gate: - **Competency gaps are now placed.** Soft skills, responsibilities, core skills, domain knowledge, and outcomes from the JD are placed into the Skills section even when the resume doesn't mention them verbatim. Only tool/platform gaps (claiming a specific tool you've never mentioned) are withheld. Years, degrees, and certifications are still blocked by `_PROTECTED_CATS`. - **Skills-first placement.** Removed synthetic bullet generation ("Drove X and Y, leveraging Z across cross-functional teams") — it read like obvious generated filler. Keywords now go to categorized Skills lines that match the resume's existing format (\textbullet separators, \item format). The weave step still handles in-place experience bullet enrichment where there's semantic overlap. - **Weave quality gates.** (a) No weaving into Skills section lines. (b) Require minimum token overlap before appending to a bullet — no more ", including SEO" on an unrelated dashboard bullet. (c) Skip verb-initial phrases for soft clauses — no more ", including analyze marketing metrics." (d) Lowercase first word of soft clause phrases. - **Skills dedup.** If "cross-functional collaboration" is listed, "cross-functional" and "collaboration" as standalone entries are dropped. Fragments like "related field", "high-quality features", solo generic words, and prepositional fragments are filtered. Verb-initial phrases are converted to noun form for Skills display. - **Custom environment support.** `_add_to_skills_section` now finds `\end{skilllist}` (not just `\end{itemize}`), with fallback to next `\section` or end of document. Measured (updated resume, honest scoring): | JD | Before | After | Ceiling | |----|--------|-------|---------| | Growth Marketing Mgr | 35.0 | 75.3 | 95.8 | | Stripe Payments PM | 38.3 | 67.8 | 86.2 | 158 tests pass, 5 pre-existing failures excluded. --- ## 2026-09-02 — FIX: ATS score was fabricated; honest scoring + no keyword injection (v2.5.0) Diagnosed against the updated résumé (`~/Documents/Resume/Saiteja_Tirunagari_Resume.tex`). The reported 80→100 scores were not real. Six defects, all confirmed by running the pipeline: - **Score denominators excluded gaps.** `mandatory_coverage` was `supported_mandatory_in_final / supported_mandatory` — so "2 of 26 must-haves, both present" read as **100%**. Empty categories defaulted to `1.0`, gifting up to 25 of 100 points for categories the résumé never addressed. An UNTOUCHED résumé scored 90/82/90 against three real JDs while genuine coverage was 30–35%. Now every denominator counts all JD criteria, and a category the JD omits is dropped with its weight redistributed (never scored as 100%). - **"Critical" was self-graded.** Criticality was read off our own calibration weights, which deliberately up-rank criteria the résumé already evidences → critical coverage was always 100%. Now taken from the JD's own importance banding. - **Gap keywords were being written into the résumé.** `place_keywords_naturally` got the full criterion set including GAPS and invented experience: *"Delivered LinkedIn Ads and email teams, via Meta Ads"*, *"Drove internal contributors and high ownership, within b2b"* — while `unsupported_insertions` still reported 0. This was the only reason scores reached the 90s. Placement is now restricted to evidence-backed criteria; gaps are disclosed, never written. Also removed the circular re-calibration that treated a just-inserted keyword as résumé evidence. - **Rewrites were ungrammatical.** Naive substring substitution spliced phrases into the middle of noun phrases: *"partner communication **skills** cadence"*, *"webinar **tools** pipeline"*. Added `_reads_naturally()` to `verify_rewrite` (one guard, all rewrite paths) rejecting a phrase followed by a content word unless that junction existed in the original. - **LaTeX was being corrupted.** Bullets were round-tripped through plain text and re-escaped, so `\textrightarrow\ ` came back as `\textbackslash\{\}` and every `\textbf{}` was lost. `_surgical_latex_edit()` now diffs the plain texts and splices only changed words into the original LaTeX, preserving markup; unlocatable edits are skipped rather than applied destructively. - **Zero rewrite candidates.** A rich résumé put every verbatim match in `already_optimized` and everything else in gaps, leaving 0 rewritable items. Partials are now candidates (with a résumé evidence anchor), and a morphology tier credits "leadership" to *"Led a team of 20"* / "analytical" to *"Product Analytics"*. Also fixed: - Extraction emitted sentence fragments, not keywords (`"knack for precise"`, `"what's not"`, `"Ability to write"`, `"running experiments (e.g"`). `_is_ats_keyword()` rejects by SHAPE (parentheticals, clause markers, pronouns, weak/verb heads, dangling function words) so unfamiliar-but-real skills still pass. 61→44 criteria on JD1. - Placement found **0 insertion points** on the new résumé: it looked only for `\end{itemize}`, but the template wraps itemize in `pbullets`/`tbullets`. Custom list environments are now discovered from `\newenvironment`. - Partial matching scanned the whole document, so `linkedin.com` in the contact line + "Ads" from "Google Ads" fabricated *"LinkedIn Ads"*. Tokens must now co-occur in one statement, and contact/URL lines are excluded. - Bulk card now shows `ATS before → after (Δ)`, `✓ N used`, `○ N ignored`, and the truthful ceiling; detail pane adds must-have coverage. New `resume.keyword_stats` in the API payload. - `test_version_bump` asserted `parts[1] >= 11`, failing on every 2.x release. Measured on the updated résumé (was: 90/82/90 on an untouched résumé, 0 rewrites): | JD | before | after | truthful ceiling | |----|--------|-------|------------------| | Stripe Payments PM | 37.3 | 45.8 | 45.8 | | Growth Marketing Mgr | 34.1 | 44.8 | 57.5 | `unsupported_insertions = 0` is now actually true. 159 tests pass; the 4 failures are pre-existing (verified identical on a clean tree) and two of them assert the gap-injection behaviour that was just removed. ## 2026-09-01 — FIX: Storage quota exceeded when saving settings (v2.4.0) - **Root cause:** `chrome.storage.local` filled by accumulated generation caches (`ats_results`, `ats_app_results`, `ats_bulk_history`) containing base64 PDFs/LaTeX from past runs. Saving settings (especially LaTeX resume) would exceed the 10MB quota. - **Fix:** `ensureQuota()` in options.js checks storage usage before save. If above 85% capacity, auto-evicts all generation caches — settings always take priority over stale results. Shows actionable error only if storage is still full after cache eviction. - Removed old PDF-only quota check that gave unhelpful "Clear existing data first" message. ## 2026-08-19 — FEAT: Support LinkedIn /jobs/search-results/ SDUI page (v2.3.3) - **Root cause of missing bulk:** `/jobs/search-results/` (preferences-based search) uses pure SDUI — no `data-occludable-job-id`, no `.scaffold-layout__list`, no `#job-details`. Cards are `