Contributor Guide
July 12, 2026 · View on GitHub
You're a GraphCompose contributor and you want to bring a brand-new document type onto the layered architecture — invoice-v2, cover-letter-v2, report-v2, anything that isn't CV. This doc is your checklist + convention reference.
It assumes you've read quickstart.md and authoring-presets.md — those explain why the layered pattern looks the way it does.
Table of contents
- The 5-layer convention
- Package map for a new family
- Naming rules
- Worked walkthrough —
invoice - Required deliverables
- Test checklist
- Doc checklist
- What you must NOT do
- PR review expectations
The 5-layer convention
Every new template family lives under
com.demcha.compose.document.templates.<family>.v2 with exactly
five sub-packages:
<family>/v2/
├── data/ records describing what's on the page
├── theme/ cosmetic tokens (palette, typography, spacing, decoration)
├── components/ internal renderers + low-level primitives
├── widgets/ reusable visual LEGO bricks
└── presets/ composition: data + theme + widgets → DocumentTemplate
Each layer's contract:
| Layer | Contract |
|---|---|
data/ | Pure records. Zero dependencies on rendering, theming, or DSL. Sealed hierarchy for section / block subtypes. |
theme/ | Records + factories. No rendering logic. Aggregate root is <Family>Theme(palette, typography, spacing, decoration). |
components/ | Static helpers. Take (host, data, theme). No statics holding state. No magic numbers — read tokens from theme. |
widgets/ | Static helpers. Named factory methods per visual variant. Compose internally from components/. |
presets/ | One public final class per visual style. Two factories: create() and create(<Family>Theme). Inner Template implements DocumentTemplate<<Family>Document>. |
The convention is the same regardless of domain. A cv has identity + sections; an invoice has parties + line items + totals; a proposal has scope + pricing + acceptance terms. The shape of the data differs; the layering is identical.
Package map for a new family
Mirror the CV v2 layout. Concrete example for invoice:
templates/src/main/java/com/demcha/compose/document/templates/invoice/
├── package-info.java ← ASCII diagram + 4-step walkthrough
├── AUTHORS.md ← recipe cookbook
├── data/
│ ├── package-info.java
│ ├── InvoiceDocument.java ← root record
│ ├── InvoiceParty.java ← from / bill-to
│ ├── InvoiceLine.java ← line item record
│ ├── InvoiceTotals.java ← subtotal / tax / total
│ ├── InvoiceSection.java ← sealed interface
│ ├── HeaderSection.java ← concrete subtype
│ ├── LineItemsSection.java ← concrete subtype
│ ├── TotalsSection.java ← concrete subtype
│ ├── NotesSection.java ← concrete subtype
│ └── Slot.java ← if multi-column variants needed
├── theme/
│ ├── package-info.java
│ ├── InvoicePalette.java ← ink / muted / rule / accent
│ ├── InvoiceTypography.java ← scale
│ ├── InvoiceSpacing.java ← margins, gaps
│ ├── InvoiceDecoration.java ← row separators, totals divider
│ └── InvoiceTheme.java ← aggregate + factories
├── components/
│ ├── package-info.java
│ ├── ParagraphPrimitive.java ← internal — package-private
│ ├── LineRowRenderer.java ← one line item row
│ ├── TotalsBlockRenderer.java ← right-aligned totals stack
│ ├── PartyBlockRenderer.java ← from / bill-to address block
│ ├── SectionDispatcher.java ← branches on sealed subtype
│ └── … (other internal renderers)
├── widgets/
│ ├── package-info.java
│ ├── Letterhead.java ← top-of-doc invoice header
│ ├── PartyPair.java ← from + bill-to side-by-side
│ ├── LineTable.java ← line items table
│ └── TotalsCard.java ← totals summary
└── presets/
├── package-info.java
├── ClassicInvoice.java ← reference preset
└── (more as added)
The structure is identical to cv — only the records inside differ.
Naming rules
- Family prefix on top-level records to avoid name collisions.
CV uses
CvName,CvIdentity,CvSection. Invoice should useInvoiceParty,InvoiceLine,InvoiceTheme. Cover letter:CoverLetterRecipient,CoverLetterTheme. Etc. <Family>Documentfor the root record. (CvDocument,InvoiceDocument,CoverLetterDocument.)<Family>Section(sealed) for the body content hierarchy.<Family>Theme(palette, typography, spacing, decoration)for the aggregate theme record.- Widgets are domain-specific verbs / nouns describing the
visual:
Headline,ContactLine,SectionHeader(CV);Letterhead,LineTable,TotalsCard(invoice). Don't try to share widget names across families — they capture different visual ideas. - Presets are descriptive proper-noun names:
BoxedSections,ModernProfessional,ClassicInvoice,MinimalLetter. Avoid generic names likeDefaultorStandard.
Worked walkthrough — invoice
Order to write things in:
1. data/ first (no rendering deps)
public record InvoiceDocument(
InvoiceParty issuer,
InvoiceParty recipient,
String invoiceNumber,
String issueDate,
String dueDate,
List<InvoiceSection> sections) {
// builder, validation, accessors
}
public sealed interface InvoiceSection
permits HeaderSection, LineItemsSection, TotalsSection, NotesSection {
String title();
}
Sealed InvoiceSection lists every body shape an invoice can have.
Concrete subtypes (LineItemsSection, TotalsSection, …) are
records carrying the section data.
2. theme/ second (no rendering, just tokens)
public record InvoiceTheme(
InvoicePalette palette,
InvoiceTypography typography,
InvoiceSpacing spacing,
InvoiceDecoration decoration) {
public static InvoiceTheme classic() {
return new InvoiceTheme(
InvoicePalette.classic(),
InvoiceTypography.classic(),
InvoiceSpacing.classic(),
InvoiceDecoration.classic());
}
}
3. components/ third (low-level renderers consume data + theme)
public final class LineRowRenderer {
private LineRowRenderer() {}
public static void render(SectionBuilder host, InvoiceLine line,
InvoiceTheme theme) {
// … DSL calls reading theme tokens …
}
}
Also: extract the shared paragraph DSL into a package-private
ParagraphPrimitive (same idea as CV) so renderers don't duplicate
configuration.
4. widgets/ fourth (named visual building blocks)
public final class LineTable {
private LineTable() {}
public static void render(SectionBuilder host,
List<InvoiceLine> lines, InvoiceTheme theme) {
for (InvoiceLine line : lines) {
LineRowRenderer.render(host, line, theme);
}
}
}
Widgets wrap components into composable units that a preset can drop into its page flow.
5. presets/ last (orchestration)
public final class ClassicInvoice {
public static final String ID = "classic-invoice";
public static final String DISPLAY_NAME = "Classic Invoice";
public static final double RECOMMENDED_MARGIN = 28.0;
private ClassicInvoice() {}
public static DocumentTemplate<InvoiceDocument> create() {
return create(InvoiceTheme.classic());
}
public static DocumentTemplate<InvoiceDocument> create(InvoiceTheme theme) {
return new Template(theme);
}
private static final class Template
implements DocumentTemplate<InvoiceDocument> {
// … compose() picks widgets in order …
}
}
Mirror the CV preset shape exactly.
Required deliverables
A new template family PR ships with:
- 5 packages under
<family>/v2/populated per convention - At least 1 reference preset that renders a sample document
-
AUTHORS.mdin the family root (recipe cookbook — copy the cv one as starting structure) -
package-info.javaat family root + each sub-package - Sample fixture in
ExampleDataFactory.sample<Family>DocumentV2() - Example runner in
examples/.../templates/<family>/v2/ - Smoke tests per the checklist below
- No edits to
engine/,dsl/, or v1<family>/surface
Test checklist
Minimum test coverage matching CV v2:
| Test class | What it asserts |
|---|---|
<Family>DocumentTest | Builder rejects null / blank required fields; valid build succeeds |
<Family>ThemeTest | All factories produce valid themes; deprecated constructors (if any) wrap correctly |
<Preset>SmokeTest | id(), displayName(), default-factory render, custom-theme render |
SectionDispatcherTest (optional) | Each sealed subtype routes correctly |
WidgetSmokeTest | Each public widget variant renders without throwing |
<Family>V2VisualParityTest | Per-pixel diff against a checked-in baseline PNG for each preset |
CI must be green before merge.
Visual regression — pixel-diff parity gate
Each preset's visual signature is frozen in a checked-in baseline PNG. A parameterised test renders the preset on A4 against a canonical sample document, rasterises each page via PDFBox, and asserts the per-pixel diff stays within a budget. Catches silent visual breakage from theme / widget / renderer refactors.
Workflow:
# 1. After a deliberate visual change — refresh baselines:
./mvnw test -Dtest='<Family>V2VisualParityTest' -Dgraphcompose.visual.approve=true
# 2. Commit the updated PNGs in the same change:
git add src/test/resources/visual-baselines/<family>-v2-layered/*.png
git commit -m "test: refresh visual baselines after <reason>"
# 3. Normal run (defends against unintended drift):
./mvnw test -Dtest='<Family>V2VisualParityTest'
Where baselines live:
core/src/test/resources/visual-baselines/<family>-v2-layered/<slug>-page-N.png
One PNG per page per preset. Pages overflow naturally — a 2-page
preset gets <slug>-page-0.png and <slug>-page-1.png.
Budget calibration — mirror the CV v2 settings until you have evidence your family needs different limits:
private static final long PIXEL_DIFF_BUDGET = 50_000L; // max mismatched pixels per page
private static final int PER_PIXEL_TOLERANCE = 8; // per-channel tolerance
These are calibrated for cross-platform PDFBox font + colour rendering drift between Windows-recorded baselines and Linux CI. Helvetica-based presets (e.g. ModernProfessional) hit ~40k mismatched pixels on the Linux CI; PT-Serif-based presets (BoxedSections, MinimalUnderlined) stay under 10k. The 50k budget covers both with margin.
If CI flakes on a specific preset above 50k, widen the budget for
that preset specifically (e.g. via a per-test Map<String, Long>
of overrides) rather than relaxing the global setting.
Failure mode: when the diff exceeds budget, the harness writes
<slug>-page-N.actual.png and <slug>-page-N.diff.png next to the
baseline so a reviewer can see exactly what changed before deciding
to re-bless or fix.
Reference: see
core/src/test/java/com/demcha/compose/document/templates/cv/presets/CvV2VisualParityTest.java
— a 200-line drop-in template you can copy for a new family.
Doc checklist
-
<family>/v2/package-info.java— ASCII diagram of the 5 layers, plus a 4-step "how to author a document" walkthrough (copy the cv one's structure). -
<family>/v2/AUTHORS.md— recipe cookbook. At least: change a glyph, change colours, add a new section subtype, conditional sections. -
<family>/v2/<sub-package>/package-info.java— each sub-package gets a paragraph explaining its role. - Update
docs/templates/v2-layered/README.mdto list the new family in the "implementations" section.
What you must NOT do
- ❌ Don't edit the engine (
document/api,document/dsl,document/engine,document/node,document/style). If your family needs an engine feature that doesn't exist, that's a separate prerequisite PR. - ❌ Don't edit v1 surface for the same family. They coexist.
Mark v1
@Deprecatedonly after the v2 surface is feature-complete and shipped — that's a follow-up PR. - ❌ Don't fork widgets across families. If invoice needs a
Headline, writetemplates/invoice/widgets/Letterhead.java— an invoice letterhead has different needs from a CV name headline. Don't try to share the same widget class across domains. - ❌ Don't use inheritance. All records are sealed, all
components / widgets / presets are
finalwith private constructors. The intentional API shape. - ❌ Don't add
instanceofon the sealed section type outsideSectionDispatcher. The dispatcher is the single dispatch point. - ❌ Don't hard-code colours / fonts / sizes in components or
widgets. Every value reads from the theme. Preset-specific
accents (a single colour used only by one preset) may live as
private static finalin that preset — but only when no other preset needs them.
PR review expectations
A new template family PR is reviewed against:
- Layer discipline — do data / theme / components / widgets / presets each obey their contract?
- Test coverage — smoke tests for every public surface; builder validation tests; theme factory tests.
- Doc completeness —
package-info.javaeverywhere,AUTHORS.mdwith at least 4 recipes, root README updated. - Visual signature — render the reference preset, attach the PDF to the PR description, eyeball-validate it matches the intent.
- No engine / v1 edits — additive only.
Expected size: ~1500-2500 lines of new code for a fresh family. Compare to cv baseline (PR #45) which was 2082 lines including 35 files.
See also
- The CV v2 package
(
com.demcha.compose.document.templates.cv) is the reference implementation. Read it end-to-end before starting a new family — every convention listed here is visible there. - authoring-presets.md — how preset authors use widgets. Same conventions apply when designing widgets for a new family.
- using-templates.md — what end users see. Your new family's API should feel consistent with this.