Skip to content

fix(templates): head every slot with the section's own title - #590

Merged
DemchaAV merged 1 commit into
feature/cv-constructor-layerfrom
feat/cv-slot-headings
Aug 21, 2026
Merged

fix(templates): head every slot with the section's own title#590
DemchaAV merged 1 commit into
feature/cv-constructor-layerfrom
feat/cv-slot-headings

Conversation

@DemchaAV

Copy link
Copy Markdown
Owner

Why

ModularCvTemplate promises that a section assembled at runtime reaches the page under the
heading its author wrote. Three of the presets that are about to declare it did the
opposite: Sidebar Portrait headed all six of its slots with a label chosen when the
preset was written, Monogram Sidebar its sidebar skills block, Mint Editorial its
profile and its skill bars. A CV whose section is titled "Employment History" reached the
page as EXPERIENCE; "Education & Certifications" as EDUCATION.

The rename is invisible in a CV whose headings happen to match the preset's vocabulary,
and wrong in every other one — and a CV assembled from data carries headings nobody in the
library chose, which is the reason its author wrote them.

What

Every slot that draws a section prints that section's title. Two blocks keep a label,
because neither draws the section it was handed:

  • Sidebar Portrait's language list picks the language rows out of an "Additional
    Information" section. Three of that fixture's four rows reach no page at all, so the
    section's title there would head content the reader cannot see. The block takes the
    title whenever every row it was given was drawn — the normal case for a section routed
    there by its role rather than by a keyword.
  • Mint Editorial draws one skills section as two adjacent blocks: an index of the group
    names, then a bar per skill. They cannot both carry one title, so the author's heads the
    index — the block a reader reaches first — and the section opens in their words even when
    the column flow puts the bars on the next page. The bars carry the preset's own "Skills",
    and nothing at all when the section is itself titled that.

Monogram Sidebar's four main slots already read the title, through a
title().isBlank() ? "…" : title() whose blank branch could not run: every CvSection
rejects a blank title in its constructor. Those branches are gone.

Wrapping. A longer heading also has to fit a narrower column. Letter-spacing inserts a
space between every pair of letters and wrapping breaks on whitespace, so
"EDUCATION & CERTIFICATIONS" came out as "EDUCATION & CERTI / FICATIONS".
TextOrnaments.spacedUpper now uses U+00A0 for the gap inside a word:
Character.isWhitespace is false for it, so a break falls between words wherever a
boundary exists, and a word wider than its column is still split. Sidebar Portrait's
character-for-character copy of that helper is deleted — the duplicate is what would have
let the fix reach one preset and not the two others this change gives long author-written
headings to.

Tests

SlotHeadingFidelityTest renders each preset exactly the sections its slots claim plus a
"Publications" section the tail is guaranteed to draw, and asserts every slot heading
appears above that marker. The leftover tail prints titles verbatim too, so without the
ordering a title found anywhere in the document would prove nothing about the slot.

Two existing tests changed for reasons worth naming:

  • LeftoverSectionTest read placement off the heading — the slot's label against the
    tail's title — which is exactly the signal this change removes. It reads placement off
    the order now, against a section that can only be in the tail: that asserts the position
    directly rather than by proxy.
  • The continuation-page control in SidebarPortraitContentFidelityTest pinned the unruled
    page to a literal read off one rendering (< 6.0), and the moved content made it 8.0. It
    measures the difference the page-margin rule makes instead, so it attributes the inset to
    the rule exactly and does not need loosening the next time the fixture shifts.

Every behavioural site was reverted one at a time to check the guard goes red — 14 of them,
including both directions of the language rule and the wrapping fix. All 14 red.

Measured, not assumed. The pixel gate stayed green throughout: the change moves 12,652
pixels on the Sidebar Portrait page against a 50,000 budget sized for cross-platform font
drift, so it would have shipped unnoticed. Setting the budget to 0 showed the three presets
had zero mismatch before this change while thirteen untouched ones differ by 69–47,959,
which is what makes the numbers above attributable. The U+00A0 gap on its own renders
0 changed pixels, and the cover letters — which share the ornament — report identical
numbers with and without it.

Five baselines re-recorded (sidebar_portrait ×2, monogram_sidebar, mint_editorial ×2);
the twelve visual.approve rewrote as collateral were reverted, which mattered here because
committing them would have baked this machine's drift in as the reference. Four committed
previews refreshed. Full reactor gate green.

composedText also stood verbatim in three test classes; it moves to CvComposedText
alongside the normaliser the assertions share, and the copies are gone.

Three presets renamed the author's sections. Sidebar Portrait headed all six
of its slots with a label chosen when the preset was written — "Employment
History" reached the page as EXPERIENCE and "Education & Certifications" as
EDUCATION — Monogram Sidebar did it to its sidebar skills block, and Mint
Editorial to its profile and its skill bars. The rename is invisible in a CV
whose headings happen to match the preset's vocabulary and wrong in every
other one, and a CV assembled at runtime carries headings nobody in the
library chose, which is the reason its author wrote them.

Every slot that draws a section now prints that section's title. Two blocks
keep a label because neither draws the section it was handed. Sidebar
Portrait's language list picks the language rows out of an "Additional
Information" section — three of that fixture's four rows reach no page at
all — so its title there would head content the reader cannot see; the block
takes the title whenever every row it was given was drawn. Mint Editorial
draws one skills section as two adjacent blocks, an index of the group names
and then a bar per skill, which cannot both carry one title: the author's
heads the index, the block a reader reaches first, so the section opens in
their words even when the column flow puts the bars on the next page.

Monogram Sidebar's four main slots already read the title through a
title().isBlank() ternary whose blank branch could not run — every CvSection
rejects a blank title in its constructor — and those branches are gone.

A longer heading also has to fit a narrower column. Letter-spacing inserts a
space between every pair of letters and wrapping breaks on whitespace, so
"EDUCATION & CERTIFICATIONS" came out as "EDUCATION & CERTI / FICATIONS".
TextOrnaments.spacedUpper now uses U+00A0 for the gap inside a word:
Character.isWhitespace is false for it, so a break falls between words
wherever a boundary exists, and a single word wider than its column is still
split. Sidebar Portrait's character-for-character copy of that helper is
deleted — the duplicate is what let the fix reach one preset and not the two
others this change gives long author-written headings to.

SlotHeadingFidelityTest renders each preset a document of exactly the
sections its slots claim plus one the tail is guaranteed to draw, and asserts
every slot heading appears above that marker: the leftover tail prints titles
verbatim too, so without the ordering a title found anywhere would prove
nothing about the slot. LeftoverSectionTest used to read placement off the
heading — the slot's label against the tail's title — which this change
removes as a signal; it reads placement off the order now, which asserts the
position directly rather than by proxy. The continuation-page control in
SidebarPortraitContentFidelityTest measured a literal read off one rendering
and the moved content invalidated it; it measures the difference the
page-margin rule makes instead, which attributes the inset to the rule and
does not need loosening when the fixture shifts.

composedText stood in three test classes verbatim; it moves to CvComposedText
with the normaliser the assertions share, and the copies are gone.
@DemchaAV
DemchaAV merged commit 1a790fd into feature/cv-constructor-layer Aug 21, 2026
12 checks passed
@DemchaAV
DemchaAV deleted the feat/cv-slot-headings branch August 21, 2026 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant