GraphCompose follows Semantic Versioning (major.minor.patch). This page is the user-facing contract for which parts of the public surface that promise covers, what breaking changes are allowed in each release type, and how sealed hierarchies, deprecations, and unannounced internal changes are handled.
The mechanism side of the same decision — how @Internal is wired up
and which guard tests enforce it — lives in
ADR-0003. This page is
the policy that ADR's mechanism enforces.
Every public class, method, field, and annotation that lives under
com.demcha.compose.* falls into exactly one of six tiers. The tier
is signalled by the package it lives in (with one exception, @Internal,
which can appear on individual elements too) and by an explicit
annotation marker where one exists.
The Supported and Legacy tiers mirror the same labels used in
docs/templates/which-template-system.md § 1:
this page is the package-wide version of that template-only status
matrix.
| Tier | Marker | Used for | Breaking changes allowed in |
|---|---|---|---|
| Stable | (default — no annotation) | The canonical authoring surface that user code is meant to call: GraphCompose.document(...), DocumentSession, DocumentDsl, RowBuilder / SectionBuilder / ParagraphBuilder and friends, DocumentInsets / DocumentColor / DocumentTextStyle, the BrandTheme factories, and the layered template presets (templates.cv.*, templates.coverletter.*, templates.invoice.*, templates.proposal.*). |
Major releases only. |
| Supported | (no annotation; called out in the page's Javadoc) | A canonical surface that ships through a major line but won't be in the next one — its replacement is already the Stable path. Bug fixes + behaviour-preserving refactors only. (No package holds this tier on the 2.0 line; the classic cv.presets.* CV surface held it through 1.x and was removed in 2.0 per which-template-system.md.) |
Minor releases for behaviour-preserving refactors; removed wholesale in the next major. |
| Extension SPI | @Beta |
Public extension points that authors are expected to implement, not only call: render-handler interfaces, NodeDefinition, custom Theme subtype contracts, fragment payload interfaces designed for extension. |
Minor releases, with a one-minor deprecation window where possible. |
| Experimental | @Beta (same annotation as Extension SPI; the distinction lives in the docstring on the annotated element) |
A brand-new public type shipping in its first minor release before its contract has stabilised. The contract is in active flux. | Any minor release, including removal. No deprecation window. |
| Internal | @Internal (per-element or per-package) |
Engine surface: everything in com.demcha.compose.document.layout.*, com.demcha.compose.engine.*, render-pipeline payload records, LayoutCompiler, NodeDefinitionSupport, the placement / measure / split contracts. Technically public for cross-package collaboration; not part of the contract. Canonical list lives in ADR-0003 § Coverage. |
Any release. No deprecation window, no CHANGELOG entry required. |
| Legacy | (no annotation; flagged in which-template-system.md and in CHANGELOG ### Deprecations) |
Pre-rebuild surface kept only so callers from before a major rebuild keep compiling. Frozen — bug fixes only. (No package holds this tier on the 2.0 line; com.demcha.templates.* and com.demcha.compose.v2.* held it through 1.x and were removed in 2.0 — the migration target is the canonical DSL.) |
Removed in the next major; no patch / minor changes other than security fixes. |
Both marker annotations (
@Internaland@Beta) live in the publicdocument.apipackage and are pinned byInternalAnnotationCoverageTest,InternalAnnotationDocumentationTest, andBetaAnnotationDocumentationTest. The Extension SPI seam currently carrying@BetaisNodeDefinition; additional Extension SPI surfaces (render-handler interfaces, fragment-payload interfaces designed for extension) will gain the marker incrementally as their contract solidifies.The Experimental surface currently carrying
@Betais the fixed-layout PPTX backend, shipping its first release in 2.1.0: thedocument.backend.fixed.pptxand…pptx.handlerspackages (package-level marker plus explicit markers onPptxFixedLayoutBackend,PptxFixedLayoutBackendProvider,PptxFragmentRenderHandler,PptxRenderEnvironment,PptxCoordinates) and the PPTX convenience methods onDocumentSession(toPptxBytes,writePptx,buildPptx). Geometry identity with the PDF backend is a design invariant and will not change; the API shape around it may still move in a minor release.Six members of the otherwise-Stable PDF backend also carry
@Beta. The package is not Experimental — these are:PdfFixedLayoutBackend.renderSections/writeSections, the low-level seam that concatenates several sections into one document, whereMultiSectionDocumentviaGraphCompose.documents()is the settled entry point most callers want instead;PdfFixedLayoutBackend.Builder.deterministicin both overloads, which pinsCreationDate/ModDateand derives the/IDfrom metadata so a document renders byte-identically across runs. Determinism is off by default, and what reproducible builds depend on is the behaviour — it is the shape of the opt-in that may still move; andPdfRenderEnvironment.letterSpacedFontwith its result recordPdfRenderEnvironment.LetterSpacedFont, the seam a render handler uses to draw tracked text with the spacing in the glyph widths rather than inTcgaps. The glyph positions and text layer it produces are settled; the shape of the call — a nullable result, a face bound to one size — may still move.
- Stable — your code that imports a Stable type compiles and runs against the next 1.x.y release without code changes; behaviour is preserved across patch releases and additive in minor releases. A removal is a major-version event called out in the CHANGELOG migration section.
- Extension SPI — implementations you wrote against the SPI continue to load in any patch release. In a minor release the SPI may require small adaptations; the previous shape is
@Deprecatedfor at least one minor release first, and the CHANGELOG entry calls out the migration explicitly. - Internal — no promises. The shape can change in any release without notice; CHANGELOG entries are optional and usually omitted to keep the user-facing changelog focused on the public surface. If you imported an
@Internaltype, you opted out of the stability contract — please open an issue so a stable wrapper can be designed. - Experimental — no promises within minor releases. We ship Experimental APIs to gather feedback before locking the shape. Once the contract stabilises (typically by the next minor release) the annotation is dropped and the type joins Stable or Extension SPI; the CHANGELOG transition is called out explicitly.
GraphCompose uses sealed interfaces in several places to keep visitor code exhaustive. The public ones — the ones this policy actually covers — are:
ChartSize(Stable)ChartSpec(Stable)CvSection(Stable)DocumentLinkTarget(Stable)DocumentPaint(Stable)DocumentPathSegment(Stable)InlineRun(Stable)ShapeOutline(Stable)
Sealed types under @Internal packages — ParagraphSpan and
PlacementContext — are outside this policy by definition; their permit
list can change in any release without notice.
A sealed interface X permits A, B, C carries a stronger contract than
a regular interface: every implementation is known to the compiler, so
a switch (block) over the permits list can be exhaustive.
Adding a new permit is therefore a breaking change for any caller
that switches on the sealed type without a default branch — even
though it's purely additive at the source level.
- Stable sealed hierarchies are additive in minor releases only when
the new variant carries a sensible default rendering for callers
that did not switch on it. Concretely: if a caller pattern-matches
on
CvSectionand hits a newly added section variant it didn't expect, the default rendering must visually degrade gracefully — typically by delegating to the closest stable variant (oftenParagraphSection) rather than throwing. - The CHANGELOG entry for the minor release names the new permit
explicitly under
### Public APIso callers know to audit their visitor code. Example wording, from the v1.6.4 cut:Added two new public Block types —
WorkHistoryBlockandEducationBlock— that let template authors declare work-history and education entries with explicit fields. The sealedBlockpermit list grows from six to eight; existingMultiParagraphBlockwork-history strings continue to parse. - Internal sealed hierarchies have no permit-list policy. The compiler enforces exhaustiveness for engine code; the public contract doesn't surface them at all.
- Removing a permit is a major-version event for any tier other
than
@Internal.
The same policy applies to sealed classes (records and class hierarchies). The mechanism is identical.
A Stable API element marked @Deprecated is removed only in a major
release, and only after the deprecation has been in effect for at least
one full minor release.
| Tier | Minimum deprecation window | Removed in |
|---|---|---|
| Stable | ≥ 1 minor release with @Deprecated. |
Major. |
| Supported | Already deprecated by category — entire tier is removed in the next major. | Major (entire tier). |
| Extension SPI | ≥ 1 minor release with @Deprecated. |
Next minor that calls out the migration in CHANGELOG ### Public API. |
| Experimental | None required. | Any minor. |
| Internal | None required. | Any. |
| Legacy | Already deprecated by category — frozen at current shape, removed in next major. | Major (entire tier). |
Every @Deprecated element ships with a Javadoc note pointing to its
replacement. The format — illustrated with placeholder type names; the
real shape uses the actual canonical replacement:
/**
* @deprecated since 1.X.0; removed in 2.0.
* Use {@link com.demcha.compose.canonical.ReplacementType#replacement(...)} instead.
* The migration is one of:
* - same shape, different package — swap the import;
* - same name, narrower contract — adjust call sites per ADR-NNN;
* - no replacement, the problem itself moved — see CHANGELOG migration note.
* Pick the bullet that applies.
*/
@Deprecated(forRemoval = true, since = "1.X.0")
public static LegacyReturn legacyMethod(LegacyArg arg) { ... }If a migration target exists, link it with {@link ...}. If the
deprecation is "this will simply go away in 2.0 and there is no
replacement because the problem itself moved," say so explicitly in
prose so the reader knows not to look for one.
Two inventories track what is deferred to the next major:
- Template surfaces and pre-rebuild aliases — the 1.x → 2.0 removals
are done on the 2.0 line; the migration map lives in
docs/templates/which-template-system.md§ 3. - Every other public-API rework, simplification, or deprecation — the ledger below, and the table of 2.x deprecations that follows it.
A row lands here during senior review (the graphcompose-senior-review skill,
Lane 1.5 "accept a compromise" step) whenever a Stable public API ships in a shape
we already intend to change but can't break in 1.x.
The cheaper path is to ship a new or unsure shape @Beta (Experimental) instead — then
there is no debt and no row, because an Experimental API may change in any minor
release (§ 1). A row exists only for compromises that had to land in the Stable
tier, which can change only in a major release. A // TODO(v2) code comment is not
a record — it rots and nothing tracks it; the record is a row here, plus an ADR
## Consequences note when the compromise reflects a real design decision.
When the 2.0 cycle opens, a planned row's API is marked
@Deprecated(forRemoval = true, since = "1.x.0") per the format above, the deprecation
window starts, and its Status flips to deprecated 1.x.
| Element | Tier now | Status | Why the 1.x shape is a compromise | 2.0 action | ADR | Issue |
|---|---|---|---|---|---|---|
DocumentSession.pageMargins(List<PageMarginRule>) / PageMarginRule |
Stable | planned | Per-page margins resolve a block's content width by the page it begins on (the engine measures each block once, before pagination). A margin that changes the content width therefore does not re-wrap a block mid-flow across a page boundary. | Revisit a page-aware per-line/per-fragment width model so a block can re-wrap when it crosses a margin boundary, if demand warrants. | — | — |
io.github.demchaav:graph-compose single-jar packaging |
Stable | landed 2.0 | The one published jar bundled the engine, the PDFBox render backend, the POI semantic backend, zxing, and the template families, so an engine-only or bring-your-own-backend consumer still pulled all of them. | Done in 2.0. Split into per-concern lockstep modules, render backends discovered via a ServiceLoader SPI. The root coordinate is renamed graph-compose-core (the lean engine); graph-compose is kept as a back-compat wrapper over graph-compose-core + graph-compose-render-pdf, so it still renders PDF out of the box. Templates are opt-in (graph-compose-templates); DOCX / PPTX ship in graph-compose-render-docx / -render-pptx. Migration: modules guide. |
ADR 0016 | — |
Stable elements deprecated since 2.0. Each still ships, still compiles and is still held by the binary-compatibility gate below; none is removed before 3.0.
| Element | Since | forRemoval |
Replacement |
|---|---|---|---|
templates.core.text.TextOrnaments.spacedUpper(String) |
2.4.0 | true |
TextOrnaments.upper(...) for the text, with TextOrnaments.SPACED_CAPS or any DocumentLetterSpacing on the style — the tracking is drawn instead of written into the string. |
templates.data.schedule — every type (WeeklyScheduleData, WeeklyScheduleDocumentSpec, ScheduleDay, ScheduleCategory, SchedulePerson, ScheduleAssignment, ScheduleSlot, ScheduleMetricRow) |
2.4.0 | false |
templates.data.rota; each type's Javadoc names its counterpart. |
The Stable-tier promise (§ 1 — no binary breaks outside a major release) is enforced
mechanically by japicmp, run in a japicmp Maven
profile during verify on the engine module (graph-compose-core) and on
graph-compose-templates. The render backends (graph-compose-render-pdf,
-render-docx, -render-pptx) and graph-compose-testing are not gated yet.
- Baselines: the published artifacts on Maven Central.
graph-compose-coreis diffed against thejapicmp.baselineproperty incore/pom.xml: the current major's floor,2.0.0for the whole 2.x line, advancing only at the next major. Holding it at the floor (rather than the previous release) is what enforces the Stable promise: every 2.x build must stay binary-compatible with the2.0.0public surface, not merely with the last minor.graph-compose-templatesis diffed twice, one execution each: against the floor (japicmp.baseline.floor,2.0.0) and against the latest published release (japicmp.baseline.previous). The floor holds the GA surface; the previous release holds everything added since, which a floor-only diff cannot protect — a member first published in2.2.0is absent from2.0.0.cut-release.ps1 -PostReleaseOnlymoves the previous pin to the release just published, andVersionConsistencyGuardTestfails the build when a pin disagrees with the working version and the CHANGELOG.
- What fails the build: any binary-incompatible change to the public surface
against a baseline — a removed or less-accessible public method/field/type or
constructor, a changed signature, and so on. A deprecated element stays protected like
any other until a major release removes it (§ 3).
@Internalpackages (com.demcha.compose.engine.*,com.demcha.compose.document.layout.*and its render-handoff payload records) are excluded; they carry no compatibility promise (§ 1). Everytemplates.*package is Stable (§ 4), so ingraph-compose-templatesonly an element carrying the per-element@Internalmarker is excluded — none does today. Source-only incompatibilities (e.g. adding a default method to an interface) are reported but do not fail, pending a finalized 2.x source-compatibility policy. - Where it runs: the pull-request
Binary Compatibilityjob,cut-release.ps1Step 5b and the publish workflow. Each ends by checking that every execution left its report: an execution that does not run — switched off, unbound, or not selected — writes none and fails nothing. A baseline japicmp cannot resolve still leaves a report, so that case is caught only where the gate fails on it —graph-compose-templates, as the next point says.BinaryCompatibilityGateGuardTestholds the executions, their settings, the job's trigger and those checks in place. - Where a baseline comes from: a published release, never this build. japicmp resolves
a pin equal to the module's own version to the artifact the build just produced —
measured on a simulated
3.0.0cut, where it reported "No incompatible changes found while checking backward compatibility of version 3.0.0 with the previous version 3.0.0", from the reactor and from a local repository the release had been installed into alike. So both pins are held strictly older than the working version (VersionConsistencyGuardTest), every path drops our cached artifacts from the local repository before it resolves, and the publish workflow runs the gate before theinstallthat seeds that repository with the release being published. - Activity window: active for every
-SNAPSHOTcycle and every release commit of a major that has a published release of its own; agraph-compose-templatesbaseline that cannot be resolved fails the build rather than skipping its diff (japicmp's default, which the engine gate still runs with, skips it with a warning). - Opening a major: while major
Xhas no release of its own — the wholeX.0.0-SNAPSHOTcycle and theX.0.0release commit — there is nothing in-major to diff against.graph-compose-templatesthen pins the previous major's floor and its last release, andjapicmp.break.binaryisfalse: both diffs run and are reported, and a break does not fail the build, because a major is allowed to break. That is the posture the 2.0 transition ran under. TheX.0.0publish workflow therefore checks those same two diffs, report-only, against releases already on Central — it never waits forX.0.0itself and never comparesX.0.0with itself. The pins becomeX.0.0andjapicmp.break.binaryreturns totrueat the first-PostReleaseOnlyafterX.0.0is published and dated in the CHANGELOG, so strict same-major enforcement resumes with the firstX.0.1-SNAPSHOTbuild.VersionConsistencyGuardTestderives all three values from the CHANGELOG and fails the build until the poms match them.
A quick lookup so callers can classify an import without reading Javadoc per element.
| Package | Tier | Module | Notes |
|---|---|---|---|
com.demcha.compose (the GraphCompose factory class) |
Stable | graph-compose-core |
The single entry point. |
com.demcha.compose.document.api |
Stable | graph-compose-core |
DocumentSession, DocumentBuilder, PageBackgroundFill, and the @Internal marker itself live here. |
com.demcha.compose.document.dsl |
Stable | graph-compose-core |
All builder types (RowBuilder, SectionBuilder, ParagraphBuilder, etc.). |
com.demcha.compose.document.node |
Stable | graph-compose-core |
Node records (RowNode, SectionNode, ParagraphNode, ...). Sealed where relevant — see § 2. |
com.demcha.compose.document.style |
Stable | graph-compose-core |
DocumentColor, DocumentInsets, DocumentTextStyle, DocumentTransform, ... |
com.demcha.compose.document.showcase |
Stable | graph-compose-core |
FontShowcase — bundled-font preview renderer. |
com.demcha.compose.document.templates.api |
Stable | graph-compose-templates |
The DocumentTemplate<S> seam every preset factory returns. |
com.demcha.compose.document.templates.core.* |
Stable | graph-compose-templates |
The shared, family-neutral template layer — BrandTheme tokens (core.theme), neutral header bricks (core.identity), text helpers (core.text), shared widgets (core.widgets). |
com.demcha.compose.document.templates.cv.* |
Stable | graph-compose-templates |
Layered CV family — CvDocument data, components, widgets, presets. |
com.demcha.compose.document.templates.coverletter.* |
Stable | graph-compose-templates |
Layered cover-letter family. |
com.demcha.compose.document.templates.invoice.* |
Stable | graph-compose-templates |
Layered invoice family — ModernInvoice on InvoiceDocumentSpec. |
com.demcha.compose.document.templates.proposal.* |
Stable | graph-compose-templates |
Layered proposal family — ModernProposal on ProposalDocumentSpec. |
com.demcha.compose.document.templates.receipt.* |
Stable | graph-compose-templates |
Layered receipt family — ModernReceipt on ReceiptDocumentSpec. First shipped in 2.4.0. |
com.demcha.compose.document.templates.rota.* |
Stable | graph-compose-templates |
Layered rota (staff shift schedule) family — CobaltRota on StructuredRotaDocumentSpec. First shipped in 2.4.0. |
com.demcha.compose.document.templates.data.* |
Stable | graph-compose-templates |
Family-neutral document data records (invoice / proposal / receipt / rota specs; the data.schedule records are deprecated since 2.4.0 in favour of data.rota). |
com.demcha.compose.document.backend.fixed.pptx |
Experimental | graph-compose-render-pptx |
Marked @Beta at the package level — PptxFixedLayoutBackend, its builder, and PptxFixedLayoutBackendProvider. First shipped in 2.1.0. |
com.demcha.compose.document.backend.fixed.pptx.handlers |
Experimental | graph-compose-render-pptx |
Marked @Beta at the package level — the PptxFragmentRenderHandler seam and its built-in handlers. |
com.demcha.compose.document.layout.* |
Internal | graph-compose-core |
Marked @Internal at the package level. Engine surface. |
com.demcha.compose.engine.* |
Internal | graph-compose-core |
Engine surface; not part of the public contract regardless of public keyword. engine.render.pdf.* ships in graph-compose-render-pdf. |
- Pixel-stable PDF output across patch releases. The layout engine
preserves structural invariants (page count, fragment ordering,
cell-content order) under semver, but pixel-exact rendering can
shift by a few sub-pixels when PDFBox bumps, font metrics change, or
a kerning fix lands. Layout regression tests (see
LayoutSnapshotRegressionExamplein the examples README) capture structure, not pixels; visual regression tests (*VisualRegressionTest) ship with calibratedmismatchedPixelBudgetvalues rather than zero. - Bit-stable artefact bytes. PDFs include creation timestamps, resource ordering hashes, and other metadata that can vary even when output is visually identical. Compare semantically, not by file hash.
- Internal package shape across releases. See § 1, tier Internal.
- Sealed hierarchy permits' exhaustiveness across minor releases for
Stable hierarchies. See § 2. Switching on a sealed
CvSectionwithout adefaultbranch will fail to compile cleanly on the next minor release that adds a new permit — by design.
- ADR-0003 — API stability boundary and the
@Internalmarker — the mechanism side (how@Internalis wired up and the architecture guards that enforce it). - ADR-0004 — PDF fragment render handler SPI is public — a worked example of opening an Extension SPI seam.
- ADR-0015 — Layered template architecture — the architectural justification for the layered template packages in § 4 (ADR-0011 documents the removed classic predecessor).
docs/templates/which-template-system.md— the template naming history and the migration map for pre-2.0 callers; this stability policy lives one level up and covers all packages, not just templates.InternalAnnotationCoverageTestandInternalAnnotationDocumentationTest— the architecture guards that fail the build if the package-level@Internalmarker disappears fromdocument.layoutor the annotation's contract drifts from this policy.InternalEnginePackageMarkerTestingraph-compose-render-pdfand its twin ingraph-compose-render-pptx— the same guard, module-local. The engine's coverage test can only reach classes on the engine's own classpath, so it cannot seeengine.render.pdf.*; each module that may ship acom.demcha.compose.engine.*package enforces the marker in its own build, over a package list read from the source tree so a newly added package fails until it is marked.
This page is maintained in lockstep with the public surface. When a new public package lands, a sealed hierarchy gains a permit, or a deprecation crosses its window, update §1 (tier matrix), §2 (sealed policy if relevant), §3 (deprecation table), and §4 (package tier lookup) in the same commit.