Skip to content

feat(templates): assemble a CV section at runtime, not at compile time - #580

Open
DemchaAV wants to merge 3 commits into
chore/templates-japicmp-gatefrom
feat/cv-module-sections
Open

feat(templates): assemble a CV section at runtime, not at compile time#580
DemchaAV wants to merge 3 commits into
chore/templates-japicmp-gatefrom
feat/cv-module-sections

Conversation

@DemchaAV

Copy link
Copy Markdown
Owner

Stacked on #579 (base chore/templates-japicmp-gate) — both touch CHANGELOG.md, and the japicmp gate #579 adds is what proves this addition is binary-compatible. Retarget to develop once #579 merges.

Why

The four CvSection records each fix one shape when the code is written. That is the right model for a CV written in Java: you pick EntriesSection, the compiler checks it, and a project is visibly a RowsSection rather than a section that happens to hold rows.

It is the wrong model when the CV arrives as data — a form, a JSON payload, an LLM. A user who has just chosen "Volunteering, shaped like Education, with dates" cannot instantiate a different record per choice, so every shape anyone anticipated would have to become a type, and the shapes nobody anticipated stay impossible. This is not hypothetical: the CVRewriter adapter already smuggles bullets into a body string as \n- , skills into **Category:** … rows, and a project's tech stack into the label as "Name (Stack)", because the model has no field for any of them.

What changed

  • ModuleSection carries the shape as a value — a fifth permit on the sealed CvSection, alongside the four untouched records, mixable in one document. CvItem holds every optional field (only title is required); CvKind decides which are read and documents the rest as ignored, so the same item renders with or without its dates depending on the kind alone. BodyStyle decides whether a description reads as prose or bullets; SectionRole states what a section means, separately from how it draws.
  • SectionRole is not yet consumed. Sidebar routing matches headings against per-preset English keyword lists, which a CV headed Ausbildung or Навыки never matches — the role is the input that fixes it, and it ships now so a document built today needs no rewrite when the routing work lands. The docs and CHANGELOG say so rather than implying it is done.
  • ModuleRenderer lowers, it does not draw. Each kind turns items into the inputs ParagraphRenderer / RowRenderer / EntryRenderer already take, so a module and the hand-written section carrying the same content are two spellings of one document. Every field a kind ignores is dropped in this one place rather than by each renderer deciding what to skip.
  • Inline vs stacked bullets is the module's choice (BULLETS, BULLETS_STACKED), not inferred from how long a description happens to be. Inference made the canonical Projects shape — one prose sentence, stacked — unreachable, and would have made a section read one way for its first entry and another for its second.
  • A BULLETS title is not linked; BULLETS_STACKED is. The inline row bolds its label by wrapping it in markdown markers, which nest around link markup and reach the page as literal asterisks; the stacked row bolds through the text style, so the link survives. A title containing a bracket is left unwrapped — the link label pattern admits none, and the alternative is printing the URL as visible text.
  • Three ways an unrecognised section shape was lost, closed. BlueBanner and ClassicSerif threw IllegalStateException from private copies of the dispatcher; EditorialBlue's had no final branch; and SectionLookup.hasContent — which presets consult before routing, and which SectionAllocation.remaining() uses to find unplaced sections — answered false for any subtype it had not been taught, discarding the heading along with the body.
  • An undated entry no longer reserves a column for the date, which EntryRenderer's Javadoc has described since it was written.
  • The train opens as 2.3.0 (first commit): new public API is a minor, and an @since written while the poms and the CHANGELOG name different lines has two answers to choose between.

Verification

  • Full reactor gate — ./mvnw -B -ntp clean verify -pl :graph-compose-core,:graph-compose-render-pdf,:graph-compose-render-docx,:graph-compose-render-pptx,:graph-compose-templates,:graph-compose-testing,:graph-compose-qa,:graph-compose-coverage -amBUILD SUCCESS, qa 801 tests (+16 net). CvV2VisualParityTest's 16-preset baselines are untouched, which is the check that no existing render moved.
  • Parity, 13 cases (ModuleSectionParityTest): each kind against the hand-written section carrying the same content, asserting layout-snapshot equality and extracted-text equality — the snapshot carries structure without words, the text carries words without positions, and either alone passes documents the other catches. Proven non-vacuous: changing the INLINE_LIST join from ", " to " | " turns it red.
  • Coverage, 31 cases (ModuleSectionKindCoverageTest): every kind over the enum (so a kind added later fails until wired), every preset that renders any shape, no preset throwing, an ad-hoc module under its own heading, and a Cyrillic heading — asserted against the composed layout, because the CV themes draw with standard-14 Helvetica which has no Cyrillic glyphs. The hasContent regression is pinned by rendering a module under a heading every preset's keyword list contains.
  • 13 unit cases (ModuleSectionTest) on the data layer: required title, null-to-blank normalisation, dropped blank/null lines, defensive copying, and each with-style copy.
  • japicmp (both baselines, both modules) → BUILD SUCCESS; javadoc:javadoc0 warnings.

Lane: templates (data + components + three preset dispatch fixes).

Limits, deliberately left for the next PRs:

  • Eight presets guard their module slots on the section's Java type (if (!(section instanceof EntriesSection …)) return;), so a module routed there is skipped rather than drawn. noPresetFailsOnAModule pins that none of them fail; placing the content is the routing PR, together with SectionRole consumption and the end of silent drops.
  • Contact still requires phone and address. Relaxing it touches ContactLine and fifteen cover-letter mastheads, which is its own change.
  • No TABLE kind yet — nothing has needed one, and an enum constant is additive when something does.

The post-release bump always opens the next patch, so the train sat on
2.2.1-SNAPSHOT. The next release adds public API to graph-compose-templates,
which makes it a minor — and an @SInCE tag written while the poms and the
CHANGELOG name different lines has two answers to choose between, one of
which outlives the cycle.

The CHANGELOG heading records the real next version and the thirteen train
poms follow it, which is the order VersionConsistencyGuardTest pins. The
README development line and the roadmap's note of when the templates gate
ships move with them. The install snippets stay on 2.2.0 — the version
actually on Central.
The four CvSection records each fix one shape when the code is written,
which is right for a CV written in Java: you pick the record and the
compiler checks it. It is the wrong model when the CV arrives as data. A
user who has just chosen "Volunteering, shaped like Education, with
dates" cannot instantiate a different record per choice, so every shape
anyone thought of would have to become a type — and the shapes nobody
thought of stay impossible.

ModuleSection carries the choice as a value instead. One CvItem holds
every optional field — title, link, subtitle, period, location,
description — and a CvKind decides which of them are read: the same item
renders with or without its dates depending on the kind alone. BodyStyle
decides whether a description reads as prose or as bullets. SectionRole
states what a section means, separately from how it draws, which is the
input the sidebar routing needs and cannot get from an English keyword
list; no preset reads it yet.

ModuleRenderer lowers each kind onto the renderers this package already
ships rather than drawing anything itself, so a module and the
hand-written section carrying the same content are two spellings of one
document. ModuleSectionParityTest holds that node-for-node — layout
snapshot for structure, extracted text for content — for every kind.

Inline versus stacked bullets is the module's choice (BULLETS,
BULLETS_STACKED) rather than something inferred from how long a
description happens to be: a section reads one way throughout, and an
author who asked for descriptions underneath gets them whether the first
entry is one line or five.

Three ways a section shape a preset did not recognise used to be lost
are closed with it: BlueBanner and ClassicSerif threw from a private
copy of the dispatcher, EditorialBlue's had no final branch, and
SectionLookup.hasContent answered false for any subtype it had not been
taught — discarding the heading with the body, through the very fallback
that exists to place unclaimed sections. An entry with no date also stops
reserving an empty column for one, which its Javadoc has described since
it was written; no shipped fixture has a blank date, so no existing
render moves.
* Validates that every field is non-null and {@code title} is
* non-blank, drops null items, and defensively copies the list.
*/
public ModuleSection {
…able

The list copy was pinned from one side only — a caller's list cannot reach
into a built module. The other side, that the accessor hands out nothing a
caller can mutate, was left to List.copyOf's reputation.
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.

2 participants