You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: harden CV pipeline with ATS lint, page-fit, and self-check
- lint_cv.py: hard-gate deterministic ATS pre-flight checks (contact
fields, experience/education completeness) before rendering
- generate_pdf(): enforce the 1-page constraint by checking WeasyPrint's
real page count, retrying once with a compact CSS override, and
warning on stderr if still overflowing instead of shipping silently
- fit_to_one_page(): given optional content (certifications/projects/
hobbies) ranked least-to-most relevant, automatically drop and
restore it to fit 1 page without manual trial and error
- screenshot_cv() + --screenshot: render a PNG self-check next to the
PDF via Playwright, so the tailoring agent can visually QA layout
- cv-fr/cv-en templates: hide Experience/Skills/Education sections
when empty, consistent with the other optional sections
- modes/generate-cv.md: document the real Python-based tailoring
workflow (CLI alone never exposed summary/bullet/droppable overrides)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+7Lines changed: 7 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -9,8 +9,15 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
9
9
10
10
### Added
11
11
-`frontend/components/candidatures/candidatures-client.tsx` — "Masquer les offres abandonnées" checkbox above the offer list, checked by default; hides `Abandonnée` offers when no explicit status filter is set (selecting "Abandonnée" in the status dropdown still shows them)
12
+
-`scripts/lint_cv.py` — deterministic ATS pre-flight lint (`lint_cv_context()`): flags missing contact fields (name/email/phone) and experience/education entries missing title/company/period or degree/school/period. Called as a hard gate in `generate_pdf()` before rendering — raises `ValueError` listing every issue instead of silently shipping a broken CV
13
+
-`scripts/generate_pdf.py` — `screenshot_cv()` + `--screenshot` CLI flag: renders the CV HTML (CSS inlined, no file/base-url needed) to a full-page A4 PNG via Playwright, saved next to the PDF, so `modes/generate-cv.md`'s LLM agent can visually self-check layout before finalizing (long names wrapping, stack-tags overflowing, etc.)
14
+
-`scripts/generate_pdf.py` — `fit_to_one_page()` + `generate_pdf(..., droppable=...)`: given optional content (certifications/projects/hobbies) ranked least-to-most relevant to the target offer, automatically drops everything droppable, then restores it back most-relevant-first as far as 1 page allows. Industrializes the manual trim-then-refill process used to fit a real CV (8 certs, 3 projects, hobbies) onto 1 page for the TREKEA offer test — verified to reproduce the same hand-tuned result automatically
12
15
13
16
### Changed
17
+
-`scripts/generate_pdf.py` — `generate_pdf()` now renders via WeasyPrint's `HTML(...).render()` first and checks the resulting page count; a CV that overflows to 2+ pages is automatically re-rendered once with a compact CSS override (tighter margins/line-height/spacing) before falling back to a stderr warning if still over 1 page. Previously nothing enforced the "must stay 1 page" constraint — WeasyPrint would silently write a multi-page PDF
18
+
-`scripts/generate_pdf.py`'s `build_cv_context()` now drops skill categories with an empty skill list, so post-tailoring content never leaves an empty category behind
19
+
-`templates/cv-fr/cv.html.j2`, `templates/cv-en/cv.html.j2` — Experience, Skills, and Education sections are now wrapped in `{% if %}` guards, consistent with Summary/Certifications/Projects/Hobbies, instead of always rendering a section header even when empty
20
+
-`modes/generate-cv.md` — step 4 now passes `--screenshot` and step 5 has the agent read the generated PNG before confirming; documents the new lint/page-count failure modes
14
21
-`frontend/components/theme-toggle.tsx` — theme toggle button now shows a `Moon`/`Sun` icon (lucide-react) instead of "Mode sombre"/"Mode clair" text, with an `aria-label` carrying the same text for accessibility
15
22
-`frontend/package.json` — bumped `next`/`eslint-config-next``16.2.10` → `16.3.0`, clearing 9 GHSA advisories against `16.2.10` (middleware/proxy bypass, Server Actions DoS/SSRF, cache confusion, image-optimization DoS, internal endpoint disclosure); `npm audit` no longer lists `next` at all
16
23
- `requirements.txt` — bumped `cryptography` `42.0.8` → `50.0.0`, `PyJWT` `2.9.0` → `2.13.0`, `mistune` `3.0.2` → `3.3.4`, `jinja2` `3.1.4` → `3.1.6`, `python-multipart` `0.0.29` → `0.0.32`, `python-dotenv` `1.0.1` → `1.2.2`, `weasyprint` `62.3.0` → `69.0` (dropped the `pydyf==0.11.0` compatibility pin now that weasyprint 69 pulls a compatible `pydyf` on its own — resolved to `0.12.1`); `pip-audit -r requirements.txt` no longer lists any of these. All three PDF generators (`generate_pdf.py`/`generate_cover_letter.py`/`generate_prep_sheet.py`) smoke-tested end-to-end (real WeasyPrint render, not mocked) to confirm the weasyprint/pydyf/jinja2 bump didn't break PDF output. `fastapi` `0.115.12` → `0.141.1` and an explicit `starlette==1.6.0` pin (previously an unpinned transitive of `fastapi`) close the remaining advisories; `pip-audit -r requirements.txt` now reports zero findings. `pytest`/`ruff` re-verified clean after the bump (one new dev-only warning: `starlette.testclient` deprecates `httpx` in favor of `httpx2`, not acted on here)
Copy file name to clipboardExpand all lines: modes/generate-cv.md
+28-9Lines changed: 28 additions & 9 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -18,21 +18,40 @@ Generate a tailored CV PDF for a specific job offer. Called from Claude Code CLI
18
18
-`highlighted_skills`: list of skills from profile.md that match the offer keywords exactly
19
19
-`summary`: rewrite the 2-sentence summary to mirror the offer's language (role title, key domain)
20
20
-`experience.bullets`: for the NeuralVision AI/ML Engineer role, reorder bullets to lead with the most relevant ones for this offer. Do NOT invent new bullets.
21
-
4.Run the PDF generation script:
21
+
4.Summary rewrite and bullet reordering aren't exposed as CLI flags — call the Python functions directly instead of the bare CLI:
22
22
23
-
```bash
24
-
python scripts/generate_pdf.py \
25
-
--offer "<company-slug>" \
26
-
--date "<YYYY-MM-DD>" \
27
-
--highlighted <skill1><skill2> ...
23
+
```python
24
+
import sys
25
+
sys.path.insert(0, ".")
26
+
from scripts.generate_pdf import default_context, generate_pdf
27
+
28
+
ctx = default_context(lang="fr") # or "en"
29
+
ctx["summary"] ="..."# tailored summary
30
+
ctx["experience"][0]["bullets"] = [...] # reordered, never invented
5. If stderr warns the CV still exceeds 1 page after the automatic compact-layout retry, rank the optional content (certifications, projects, hobbies) least-to-most relevant to this offer and pass it as `droppable` to `generate_pdf()` — it trims from the least relevant end and restores as much as still fits 1 page, before falling back to the compact-CSS retry:
37
+
38
+
```python
39
+
droppable = [
40
+
("certification", "<least relevant cert name>"),
41
+
("certification", "<next least relevant cert name>"),
42
+
("project", "<least relevant project name>"),
43
+
("hobbies", None), # dropped as a whole unit, tried last (most relevant to keep)
5. Confirm the PDF was created at `output/cv-<company-slug>-<date>.pdf`
48
+
6. Read the generated `output/<slug>-<date>/cv-<slug>-<date>.png` screenshot (View/Read tool) and check for: text overflowing 1 page, long company/skill names wrapping badly, stack-tags spilling onto a new line. Re-tailor content and re-run if something looks broken.
49
+
7. Confirm the PDF was created at `output/<slug>-<date>/cv-<slug>-<date>.pdf`
The script raises on ATS pre-flight lint failures (missing contact fields, missing experience/education fields) and warns on stderr if the CV still exceeds 1 page after the automatic compact-layout retry — do not report success if that warning appears without fixing the content first.
0 commit comments