From ab853afc62f2e8b3912fc4a4e01e748497aa5547 Mon Sep 17 00:00:00 2001 From: Timothy Oluwatobi Ojebiyi Date: Tue, 20 Oct 2026 13:46:17 +0100 Subject: [PATCH] fix: a parser reading the CV in order finds each bullet under its role The CV's bullet marker was absolutely positioned, and WeasyPrint draws positioned boxes after the page's other text. A parser reading the PDF in order (pypdf, PyMuPDF, an ATS that indexes the file) found every experience bullet after the Skills section, cut off from its role. The marker is now an inline-block hung into the list's indent, so it is drawn in line with its text. On the candidate's CV every one of 673 words lands within 0.2pt of where it did, with the markers in the same column; the reading order is role, its bullets, Projects, Skills. A new WeasyPrint test checks that order and fails on the old styling. --- api/app/templates/base.css | 7 +++++-- api/tests/test_documents.py | 11 +++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/api/app/templates/base.css b/api/app/templates/base.css index e2808b4..ebc0ae3 100644 --- a/api/app/templates/base.css +++ b/api/app/templates/base.css @@ -73,8 +73,11 @@ body.cv { .cv-entry-title { font-weight: 700; } .cv-dates { flex-shrink: 0; margin-right: 44pt; font-size: 9pt; color: #595959; white-space: nowrap; } .cv-bullets { margin: 0; padding: 0 0 0 36pt; list-style: none; } -.cv-bullets li { position: relative; margin: 0; } -.cv-bullets li::before { content: "•"; position: absolute; left: -18pt; font-family: Arial, "Liberation Sans", sans-serif; } +.cv-bullets li { margin: 0; } +/* The bullet sits in the line, hung into the list's indent, rather than positioned: WeasyPrint draws positioned + boxes after the page's other text, so a parser reading the PDF in order found every bullet after Skills. */ +.cv-bullets li::before { content: "•"; display: inline-block; width: 18pt; margin-left: -18pt; line-height: 1; + font-family: Arial, "Liberation Sans", sans-serif; } .cv-muted { font-size: 9pt; color: #595959; } /* ── Fit to page ────────────────────────────────────────────────────── */ diff --git a/api/tests/test_documents.py b/api/tests/test_documents.py index 0bed541..dbe17a4 100644 --- a/api/tests/test_documents.py +++ b/api/tests/test_documents.py @@ -583,6 +583,17 @@ def test_the_graduate_cv_is_one_page_in_the_designs_type_with_live_links(pdf_dir assert {"mailto:sam@example.com", "https://github.com/samokafor"} <= uris +@needs_weasyprint +def test_a_parser_reading_the_cv_in_order_finds_each_bullet_under_its_role(pdf_dir): + # A parser reads the PDF in the order it was drawn. A positioned bullet marker was drawn after the page's other + # text, so every experience bullet came after the Skills section, cut off from its role. + cv = agent._persona_to_cvdoc(GRADUATE) | {"seniority": "junior"} + text = " ".join(_weasy("cv", cv)[0].get_text().split()) + order = ["Software Engineering Intern", "Built a FastAPI service that ingests 40k", "Pantry Planner", + "Skills & Technical Proficiency"] + assert [text.index(s) for s in order] == sorted(text.index(s) for s in order) + + @needs_weasyprint def test_a_full_graduate_cv_fits_one_page(pdf_dir): # Everything the tailoring prompt allows a junior: a 3-sentence summary, three roles with 2 bullets of