ADR 0011
July 12, 2026 · View on GitHub
- Status: Accepted (with Phase E.1 reopen caveat — see Consequences)
- Date: 2026-05-06
- Authors: Artem Demchyshyn
Context
The v1.5 templates layer evolved into a known mess that blocked
contributors and forked easily on every visual change. The pain
points (recorded in docs/private/templates-restructure-plan.md
sec 1.1):
- CV mixed with invoice / proposal in one folder. All 14 CV
presets, V1/V2 invoice and proposal templates, and the weekly
schedule template all sat in
templates/builtins/. - Meaningless class names.
CvTemplateV1,BlueBannerCvTemplate,EditorialBlueCvTemplate,MonogramSidebarCvTemplate— names that conveyed neither the visual style nor the domain. - One composer per template. ~15 hand-coded composer classes
under
templates/support/cv/(300-700 LOC each), each reimplementing its ownaddHeader(),addModule(),addSidebar()helpers. A new visual variant required forking a 600-line file. - Header rebuilt per composer. Every composer constructed its own contact line + link row in private methods. ~15 duplicate implementations of the same idea.
- Spacing tokens scattered across three places.
CvTheme.spacing,TemplateLayoutPolicy.rootSpacing,MINIMUM_TOP_LEVEL_MODULE_SPACINGhardcoded. No single source of truth. - Templates baked their own table.
InvoiceTemplateV2couldn't be themed because its line-items table was hardcoded in the composer. - No public builder. Authors couldn't compose a custom CV from the existing components — the only path was forking a builtin composer.
CvTemplateinterface privatised to CV data. NoDocumentTemplate<S>generic — every domain (CV / invoice / proposal / cover letter) needed its own marker interface.- Decorations baked into composers. Dividers, panels, accent strips were inline in each composer rather than a shared library.
- Layout coupled to style. Each composer hardcoded its own column count and slot positions — no way to swap the layout without rewriting the composer.
- 14 presets with duplicates / near-copies.
BlueBanner≈Editorial,BoxedSections≈CenteredHeadline. Hard to tell what's intentionally different.
The user's directive (May 2026): the templates layer is breakable in v1.6 minor — v1.x SemVer "API stability" covers the engine, not the templates. Few external users; rebuild from scratch under a clean architecture.
Decision
Rebuild the templates layer under a four-layer mental model (Theme → Layout → Components → Spec) with per-domain folders and flat copyable preset recipes. The new package layout:
templates/
api/ DocumentTemplate<S>, SlotMap
themes/ Spacing, Typography (token records)
components/ Header, Module, MarkdownText
blocks/ sealed Block hierarchy:
ParagraphBlock, BulletListBlock,
NumberedListBlock, IndentedBlock,
KeyValueBlock, MultiParagraphBlock
decorations/ Spacer, Divider, AccentStrip
cv/
layouts/ SingleColumn, TwoColumnSidebar, ThreeColumnMagazine
presets/ 14 flat copy-and-tweak preset classes
builder/ CvBuilder
spec/ CvSpec, CvHeader, CvModule
coverletter/
layouts/ LetterFormat
presets/ 14 paired letter presets
builder/ CoverLetterBuilder
spec/ CoverLetterSpec, CoverLetterHeader
invoice/
presets/ ModernInvoice (minimal v2 surface)
builder/ InvoiceBuilder
spec/ InvoiceSpec
proposal/
presets/ ModernProposal (minimal v2 surface)
builder/ ProposalBuilder
spec/ ProposalSpec
Key design contracts:
DocumentTemplate<S>generic interface.compose(DocumentSession, S spec)— replaces the oldCvTemplateand the various per-domain marker interfaces.- Flat preset recipes. Each preset is one final class with one
static DocumentTemplate<S> create(BusinessTheme)factory. No inheritance, no subclassing — copy the body ofcreate(...), tweak the builder calls, done. - Reusable components.
Header(with right-aligned variant + fluentwithNameStyle / withContactStyle / withLinkStyleoverrides),Module(with per-instanceStylerecord),Blocksealed hierarchy (paragraph, bullet, numbered, indented, key-value, multi-paragraph) — all live intemplates/components/andtemplates/blocks/. - Shared
Spacingtokens. SingleSpacing.compact()/Spacing.comfortable()/Spacing.airy()factories — replaces the three scatteredCvTheme.spacing/TemplateLayoutPolicy.rootSpacing/MINIMUM_TOP_LEVEL_MODULE_SPACINGknobs. - Slot-based layouts.
SingleColumn/TwoColumnSidebar/ThreeColumnMagazine— the slot-basedCvBuilderlets multi-column presets rearrange modules across slots through.place(slot, "Module Name", ...)calls. - Markdown-aware bodies.
MarkdownText.parse(text, baseStyle)helper renders inline**bold**/*italic*/_italic_markers asInlineRunlists with the matchingDocumentTextDecoration. Used by every CV / cover-letter preset paragraph body so spec authors carry inline emphasis without preprocessing. - Per-preset
RECOMMENDED_MARGINconstant so the gallery example wires the right page margin without magic numbers at the call site. - API break is intentional.
templates/builtins(14 V1 CV templates),templates/support/cv(15 composers),templates/theme/CvTheme, theCvTemplateinterface — all removed. Migration table inCHANGELOG.mdanddocs/roadmaps/migration-v1-5-to-v1-6.md. Cinematic V2 templates (InvoiceTemplateV2/ProposalTemplateV2) are kept until the minimal v2 surface (ModernInvoice/ModernProposal) closes feature parity in a follow-up release.
Consequences
Positive
- Per-domain folders — CV / invoice / proposal / cover letter
no longer share a flat
builtins/namespace. - Meaningful preset names —
ModernProfessional/NordicClean/BlueBanner/EditorialBlue/Executive/EngineeringResume(wasTechLead) /Panel(wasProductLeader) /SidebarPortrait/MonogramSidebar/TimelineMinimal/BoxedSections/CenteredHeadline/ClassicSerif/CompactMono. Spacingsingle source of truth — every preset reads spacing from one record.- Generic
DocumentTemplate<S>— same interface for CV, cover letter, invoice, proposal, future domains. - Markdown-aware bodies — spec authors can emit
**bold**/*italic*inline. - Examples gallery completeness —
cv-<id>.pdfandcover-letter-<id>.pdfper preset, regenerable viaexamples/CvTemplateGalleryFileExample/CoverLetterTemplateGalleryFileExample. CvHeader.jobTitle— first-class subtitle field for the presets that surface it (EditorialBlue / Panel / SidebarPortrait / MonogramSidebar).
Negative — Phase E.1 reopen caveat
The first Phase E.1 pass shipped visually-broken renders. Every
CV preset rendered as a teal-tinted single-column
ModernProfessional clone — NordicClean lost its sidebar,
BlueBanner lost its banners, MonogramSidebar lost its
monogram badge. The visual parity gate
(templates-restructure-plan.md sec 6.2) was specified as a
PNG-rasterize pixel-diff with budget 2500 but was never built:
ModernProfessionalVisualParityTest was a smoke test (file-exists
check) and PresetLayoutSnapshotTest recorded baselines from the
new (broken) v2 renders without comparing against V1.
Phase E.1 was reopened in May 2026 and all 14 CV preset renders
- 14 cover-letter pair renders were rebuilt against V1 visual references. The reopen made an explicit trade-off:
- 13 of 14 CV presets are implemented as hand-coded
DocumentTemplatesubclasses driving the canonical PageFlow DSL directly (≈ 400-700 LOC each) rather than thin recipes through the slot-basedCvBuilder. Restoring V1 visual fidelity required components the v2 library hadn't grown yet (Panel.softTinted,TwoColumnSidebar.tinted,SectionStyle.uppercaseRule,WorkEntryRenderer). - Tech debt: ≈ 5500 LOC of inlined parsing helpers
(
parseWorkEntry,parseProjectEntry,contactParts) and layout recipes. Architecturally identical to the V1 composers the restructure was supposed to retire. - Phase E.4 (deferred to v1.7) tracks the component library
extension + preset refactor that closes this gap. See
docs/private/templates-restructure-plan.mdsec 12 anddocs/private/templates-v2-audit-remediation.md.
Other consequences
- Visual parity gate is now in place.
PresetVisualParityTest(one for CV, one for cover letters) renders each preset to PDF, rasterises page 0 (andclassic_serif's page 1) via PDFBoxPDFRenderer, and asserts the per-pixel diff against a checked-in baseline PNG stays within budget 2500 mismatched pixels at per-channel tolerance 8. Baselines live undercore/src/test/resources/visual-baselines/{cv-v2,coverletter-v2}/. Re-bless after a deliberate visual change with-Dgraphcompose.visual.approve=true. The harness (PdfVisualRegression) was already built but never wired into the templates layer; Phase E.1 reopen plugged the 28 presets into it. - Migration is breaking. Anyone on
new CvTemplateV1()/new NordicCleanCvTemplate()etc. must switch to the new factory (see migration table inCHANGELOG.mdanddocs/roadmaps/migration-v1-5-to-v1-6.md). - Cinematic V2 templates remain.
InvoiceTemplateV2/ProposalTemplateV2/WeeklyScheduleTemplateV1stay intemplates/builtins/. The builtins folder is not yet empty; final cleanup happens onceModernInvoice/ModernProposalclose cinematic feature parity. templates/data/is partially deferred.data/cv/CvDocumentSpec.javaanddata/coverletter/CoverLetterDocumentSpec.javaare dead code referenced only by tests;data/invoice/*/data/proposal/*/data/schedule/*are still consumed by V1/V2 composers and stay until those are retired.
Status
- Implemented through Phase A → Phase G in May 2026.
- Phase E.1 reopened May 2026 to recover V1 visual fidelity. Tech
debt acknowledged; preset refactor (hand-coded subclasses → thin
CvBuilderrecipes) scheduled for v1.7 (Phase E.4). - Visual parity gate landed as part of the reopen
(
PresetVisualParityTest+ 28 baseline PNGs).