feat(templates): assemble a CV section at runtime, not at compile time - #580
Open
DemchaAV wants to merge 3 commits into
Open
feat(templates): assemble a CV section at runtime, not at compile time#580DemchaAV wants to merge 3 commits into
DemchaAV wants to merge 3 commits into
Conversation
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.
This was referenced Aug 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The four
CvSectionrecords each fix one shape when the code is written. That is the right model for a CV written in Java: you pickEntriesSection, the compiler checks it, and a project is visibly aRowsSectionrather 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
ModuleSectioncarries the shape as a value — a fifth permit on the sealedCvSection, alongside the four untouched records, mixable in one document.CvItemholds every optional field (onlytitleis required);CvKinddecides which are read and documents the rest as ignored, so the same item renders with or without its dates depending on the kind alone.BodyStyledecides whether a description reads as prose or bullets;SectionRolestates what a section means, separately from how it draws.SectionRoleis not yet consumed. Sidebar routing matches headings against per-preset English keyword lists, which a CV headedAusbildungorНавыки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.ModuleRendererlowers, it does not draw. Each kind turns items into the inputsParagraphRenderer/RowRenderer/EntryRendereralready 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.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.BULLETStitle is not linked;BULLETS_STACKEDis. 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.BlueBannerandClassicSerifthrewIllegalStateExceptionfrom private copies of the dispatcher;EditorialBlue's had no final branch; andSectionLookup.hasContent— which presets consult before routing, and whichSectionAllocation.remaining()uses to find unplaced sections — answeredfalsefor any subtype it had not been taught, discarding the heading along with the body.EntryRenderer's Javadoc has described since it was written.@sincewritten while the poms and the CHANGELOG name different lines has two answers to choose between.Verification
./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 -am→ BUILD SUCCESS, qa 801 tests (+16 net).CvV2VisualParityTest's 16-preset baselines are untouched, which is the check that no existing render moved.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 theINLINE_LISTjoin from", "to" | "turns it red.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. ThehasContentregression is pinned by rendering a module under a heading every preset's keyword list contains.ModuleSectionTest) on the data layer: required title, null-to-blank normalisation, dropped blank/null lines, defensive copying, and eachwith-style copy.javadoc:javadoc→ 0 warnings.Lane: templates (data + components + three preset dispatch fixes).
Limits, deliberately left for the next PRs:
if (!(section instanceof EntriesSection …)) return;), so a module routed there is skipped rather than drawn.noPresetFailsOnAModulepins that none of them fail; placing the content is the routing PR, together withSectionRoleconsumption and the end of silent drops.Contactstill requires phone and address. Relaxing it touchesContactLineand fifteen cover-letter mastheads, which is its own change.TABLEkind yet — nothing has needed one, and an enum constant is additive when something does.