What happens
A CV generated through the web pdf worker renders a section title, and its rule, for every optional section the agent leaves empty. For a candidate with no projects, no awards and no interests, the PDF carries three headings with nothing underneath them.
Why
stripEmptySections in cv-sections-core.mjs is what removes those, and it has exactly two callers:
build-cv-html.mjs:676: html = stripEmptySections(html, payload, 'html');
build-cv-latex.mjs:214: template = stripEmptySections(template, payload, 'tex');
Since #2185 the web pdf path uses neither builder. The agent fills templates/cv-template.html itself and emits the finished HTML inline, the backend writes it with writeCvHtml (web/src/lib/pdf-render.mjs:79, a plain fs.writeFileSync), and generate-pdf.mjs renders that file. Nothing along that path calls stripEmptySections, so an unfilled section keeps the static heading markup the template carries:
<!-- PROJECTS -->
<div class="section">
<div class="section-title">Projects</div>
</div>
There is a second reason it cannot simply be called there. stripEmptySections decides emptiness with isEmptySection(payload, section), which inspects a payload the web path never constructs, since the agent emits rendered HTML rather than a payload.
Reproduce
Run pdf mode from the web UI for a candidate whose CV has no projects, awards or interests. The rendered PDF shows PROJECTS, AWARDS & HONORS and INTERESTS as headings with rules and no content. The same candidate through build-cv-html.mjs renders correctly, because the stripper runs there.
Seen on v1.32.0, web 0.10.0.
Impact
Beyond looking unfinished on a document that goes to an employer, the empty sections take up enough vertical space to push the CV past its page budget. On the CV that prompted this, removing the three empty sections and changing nothing else took it from three pages to two, so the length warning was a downstream symptom of this bug rather than a content problem.
This is the same class as #2119, #2512, #2516 and #2072, each of which removed a bare header in the builder path. The web path was never covered by any of them.
Suggested fix
The section boundaries in PATTERNS.html are keyed on the templates' all-caps section comments, which the agent's output preserves, so they already match agent-emitted HTML. Only the emptiness predicate needs an HTML counterpart.
The smallest version reuses the existing public API unchanged: derive an emptiness map by reading each rendered section body, then hand that to stripEmptySections so the vetted patterns still do the removal. I tested that against a real agent-emitted CV and it removed exactly projects, awards and interests while leaving competencies, experience, education, certifications and skills untouched.
A cleaner home for it would be a content-driven strip exported from cv-sections-core.mjs beside the payload-driven one, so both paths share one definition of where a section starts and ends rather than the web path growing its own.
Happy to open a PR if the approach looks right.
What happens
A CV generated through the web
pdfworker renders a section title, and its rule, for every optional section the agent leaves empty. For a candidate with no projects, no awards and no interests, the PDF carries three headings with nothing underneath them.Why
stripEmptySectionsincv-sections-core.mjsis what removes those, and it has exactly two callers:Since #2185 the web
pdfpath uses neither builder. The agent fillstemplates/cv-template.htmlitself and emits the finished HTML inline, the backend writes it withwriteCvHtml(web/src/lib/pdf-render.mjs:79, a plainfs.writeFileSync), andgenerate-pdf.mjsrenders that file. Nothing along that path callsstripEmptySections, so an unfilled section keeps the static heading markup the template carries:There is a second reason it cannot simply be called there.
stripEmptySectionsdecides emptiness withisEmptySection(payload, section), which inspects a payload the web path never constructs, since the agent emits rendered HTML rather than a payload.Reproduce
Run
pdfmode from the web UI for a candidate whose CV has no projects, awards or interests. The rendered PDF shows PROJECTS, AWARDS & HONORS and INTERESTS as headings with rules and no content. The same candidate throughbuild-cv-html.mjsrenders correctly, because the stripper runs there.Seen on v1.32.0, web 0.10.0.
Impact
Beyond looking unfinished on a document that goes to an employer, the empty sections take up enough vertical space to push the CV past its page budget. On the CV that prompted this, removing the three empty sections and changing nothing else took it from three pages to two, so the length warning was a downstream symptom of this bug rather than a content problem.
This is the same class as #2119, #2512, #2516 and #2072, each of which removed a bare header in the builder path. The web path was never covered by any of them.
Suggested fix
The section boundaries in
PATTERNS.htmlare keyed on the templates' all-caps section comments, which the agent's output preserves, so they already match agent-emitted HTML. Only the emptiness predicate needs an HTML counterpart.The smallest version reuses the existing public API unchanged: derive an emptiness map by reading each rendered section body, then hand that to
stripEmptySectionsso the vetted patterns still do the removal. I tested that against a real agent-emitted CV and it removed exactly projects, awards and interests while leaving competencies, experience, education, certifications and skills untouched.A cleaner home for it would be a content-driven strip exported from
cv-sections-core.mjsbeside the payload-driven one, so both paths share one definition of where a section starts and ends rather than the web path growing its own.Happy to open a PR if the approach looks right.