feat: add ATS & keyword verification pass to /apply's compile-and-inspect loop (#40)

An ATS reads the compiled PDF's embedded text layer, not the rendered page,
and LaTeX can silently produce PDFs whose text extracts as garbage: icon
glyphs where contact details should be, (cid:*) markers from fonts without
Unicode mappings, interleaved lines from multi-column layouts. This matters
more now that /add-template lets users bring arbitrary templates. The
existing Step 5 loop verifies what a human sees; this adds verification of
what a parser sees.

New Step 5d in /apply (CV only - cover letters rarely go through keyword
screening; cleanup renumbered to 5e):

- Extract the CV PDF's text layer with pdftotext -layout. pdftotext
  (poppler) is an optional dependency: if missing, the mechanical check is
  skipped with a warning and keyword coverage falls back to the visual PDF
  read - the same graceful-skip pattern as salary_lookup.py
- Parseability checks verified against a real extraction of the stock
  template: email/phone must survive as literal text (fontawesome icons
  extract as harmless glyph-name noise like MOBILE-ALT/Envelope, but a
  contact detail carried only by an icon or hyperlink is invisible to ATS),
  no (cid:*) or replacement-character garbage, reading order matching
  visual order, dates present
- Keyword coverage reuses the required/preferred list from Step 1, matched
  in the posting's language, reported as covered / synonym-only /
  missing-have-it / missing-gap. Honesty rule enforced: keywords the
  profile genuinely supports get added to experience bullets; genuine gaps
  stay visible, never stuffed

Integration: CLAUDE.md verification checklist section, ATS Parseability
guidance in 05-cv-templates.md, narrow Bash(pdftotext:*) entry in the
pre-approved permissions (keeping with the tightened scope from #27),
cv/*.txt gitignored (extraction is personal data; also deleted by the
step itself), and optional-dependency docs in README and SETUP.
This commit is contained in:
Ayobami Adegoke
2026-07-07 06:28:59 +02:00
committed by GitHub
parent b05fa05b4a
commit 2551824ecb
7 changed files with 78 additions and 3 deletions
+7
View File
@@ -126,3 +126,10 @@ Both documents MUST be compiled and visually inspected via the Read tool on the
- [ ] **No orphaned `\cventry` titles** - a job/education title must never sit at the bottom of a page with its bullets spilling to the next page. Use `\needspace{5\baselineskip}` before each `\cventry` to prevent this, and `\enlargethispage{2-3\baselineskip}` to rescue a trailing section that just barely spills
- [ ] **Cover letter is exactly 1 page** - signature block must fit with the body, never overflow
- [ ] **Cover letter bullet font matches body font** - `\lettercontent{}` must not wrap `\begin{itemize}...\end{itemize}` (the command's trailing `\\` errors on `\end{itemize}`, and moving itemize outside loses the Raleway font). Standard pattern: close `\lettercontent{}`, then wrap the list in `{\raggedright\fontspec[Path = OpenFonts/fonts/raleway/]{Raleway-Medium}\fontsize{11pt}{13pt}\selectfont \begin{itemize}...\end{itemize}\par}`
### ATS & keyword verification (CV)
ATS parsers read the PDF's embedded text layer, not the rendered page. Extract it with `pdftotext -layout` and verify what a parser sees. `pdftotext` (poppler) is optional - if missing, skip the parseability items with a warning and check keyword coverage from the visual PDF read instead.
- [ ] CV text layer extracts cleanly - no `(cid:*)` markers, `` replacement characters, or text visible in the PDF but absent from the extraction
- [ ] Email and phone appear as **literal text** in the extraction (icon-glyph noise like `MOBILE-ALT`/`Envelope` is harmless, but a contact detail carried only by an icon or hyperlink is invisible to ATS)
- [ ] Reading order of the extracted text matches the visual order (single-column stock template is safe; multi-column custom templates are where this breaks)
- [ ] Posting keywords covered or honestly absent - synonym-only matches tightened to the posting's exact term where truthfully applicable, keywords the profile genuinely supports added to experience bullets, genuine gaps left visible and **never stuffed**