Releases: DemchaAV/GraphCompose
GraphCompose v1.6.9
v1.6.9 — 2026-06-03
Housekeeping cycle plus the public pixel-level visual-regression API (Track N).
Public API
- Promoted the pixel-level visual-regression harness to public API.
com.demcha.compose.testing.visual.PdfVisualRegressionand
com.demcha.compose.testing.visual.ImageDiff(@since 1.6.9) move from the
test source set intosrc/main/java, alongside the existing
com.demcha.compose.testing.layout.*semantic snapshot helpers. Library
consumers can now run the same render-PDF → diff-PNG baseline gate against
their own presets and templates instead of copying the harness. Behaviour is
unchanged; the PDF→image step is inlined on PDFBox'sPDFRenderer. - Exposed
PdfVisualRegression.APPROVE_PROPERTY(@since 1.6.9) — the
graphcompose.visual.approvesystem-property name — so consumers can toggle
baseline-approve mode without hard-coding the string (mirrors
LayoutSnapshotAssertions.UPDATE_PROPERTY).
Documentation
- Added
docs/operations/visual-regression-testing.md:
pixel-vs-semantic guidance, thePdfVisualRegressionAPI, approve mode,
baseline layout, and cross-platform tolerance calibration. - README "Which API should I use?" gains a pixel-level visual-regression row.
- Made the entire
com.demcha.compose.document.*public API Javadoc
doclint-clean. Added the missing@param/@return/@throwstags and
element descriptions across 142 files somvn javadoc:javadoc
(doclint=all) runs warning-free. Java's default-Xmaxwarns=100cap had
masked ~90% of the gaps (true count: 929 warnings, not the ~100 first
visible). Additive Javadoc only — no behaviour change; the only code
additions are 16 behaviour-neutral no-arg constructors in
layout/definitions/*(documenting the otherwise-synthesised public default
constructor) and removal of the@deprecatedblock-tagsdoclintforbids in
package-info.java(the@Deprecatedannotation + prose body already carry
the notice).
Build
- CI Javadoc validation (
maven-javadoc-plugin,doclint=all) now covers the
publiccom.demcha.compose.testing.*helpers (testing.layout+testing.visual)
in addition to the canonicaldocumentAPI, so Javadoc regressions on the
testing surface fail fast in CI. No artifact or behaviour change. - Bumped
central-publishing-maven-plugin0.9.0 → 0.10.0 (the Maven Central
publishing plugin) and removed the Dependabot block on 0.10.0; the
release-profile build is verified locally and the Central upload is exercised
at the next publish.
GraphCompose v1.6.8
v1.6.8 — 2026-06-01
CV v2 migration completion + design-token expansion. v1.6.8
finishes the CV v2 migration with hyperlink-aware project / entry
titles: a row authored as "[GraphCompose](https://github.com/x/y) (Java, PDFBox)" now renders the title as a clickable link in the
final PDF, with the technology stack remaining a plain
(Java, PDFBox) tail. The mechanism is a small extension to
the inline-Markdown parser used by every CV / cover-letter body
row — the [label](url) syntax produces a RichText.link(...)
run; bare brackets stay literal; everything else (**bold**,
*italic*, _italic_) keeps working as before. The release also
ships four contemporary BusinessTheme factory presets
(nordic(), editorial(), cinematic(), monochrome())
alongside the classic / modern / executive trio, expanding the
built-in design-token range to seven presets. Senior-review
follow-ups from v1.6.7 round out the release: the two registry
mutation entry points on DocumentSession are now fully
interchangeable (both refuse to mutate a closed session and both
invalidate the layout cache), target-branch: develop is pinned
in Dependabot config so future bumps land on the integration
branch, and logback-classic rolls forward to 1.5.34 which
fixes CVE-2026-9828
(deserialisation whitelist bypass).
Zero breaking public API changes. The japicmp gate against
the v1.6.7 baseline reports semver PATCH, compatible bug fix
across every PR in the cycle. New BusinessTheme factories are
pure additions; MarkdownInline.append and plainText extend
their behaviour without changing their signatures; ProjectLabel. parse keeps its two-field record shape (the title() field now
preserves Markdown rather than returning a pre-flattened
projection, but the type contract is unchanged and the visible
text projection is one call away via MarkdownInline.plainText( title)). 1058 tests pass at the release-prep tip.
Migration from v1.6.7. No code changes required for typical
usage. If you build a custom renderer on top of
ProjectLabel.parse:
- Old
title()was already the visible plain text (emphasis +
link syntax stripped). Newtitle()preserves the original
inline-Markdown. Wrap withMarkdownInline.plainText(...)to
recover the old behaviour, or route through
MarkdownInline.append(rich, title, style)to get
emphasis / link rendering for free (the same path
ProjectRenderernow uses). MarkdownInline.appendconsumers automatically pick up link
rendering for[label](url)syntax. If any CV / cover-letter
fixture in your codebase contained a literal[...](...)
string that previously rendered as text, it will now render
as a hyperlink. Escape with HTML entities or restructure the
string if you need to keep it literal.
The next release is v1.7.0 — the additive canonical-DSL
feature minor (LineBuilder.dashed, inline shapes, TimelineBuilder,
dx shortcuts, recipes docs). See ROADMAP.md.
Fixes
- The two
DocumentSessionregistration entry points are now
fully interchangeable, not just cache-equivalent.
session.registry().register(...)now callsensureOpen()
before mutating, matching the behaviour of
session.registerNodeDefinition(...). Previously
registry().register(...)on a closed session silently mutated
the registry and invalidated a closed-session cache (harmless
but semantically odd). After this change both paths throw
IllegalStateExceptionon a closed session. (Track J2 — carry-
over polish from the v1.6.7 senior review.)
Internal
NodeRegistryJavadoc updated to call out the v1.6.7 non-final
relaxation explicitly (Track J4). The class became non-final
in v1.6.7 (Track I3) soDocumentSessioncould install the
auto-invalidating subclass; the change was already binary-
compatible (japicmp classified it assemver PATCH). The
Javadoc just makes the rationale discoverable without reading
the CHANGELOG.
Public API
MarkdownInline.append(...)(the inline-markdown adapter used by
every CV / cover-letter body / row / entry renderer) now
recognises standard Markdown link syntax[label](url)and emits
a clickable hyperlink run viaRichText.link(label, url). Pure
parser extension — noCvRowdata-shape change required.
MarkdownInline.plainText(...)is updated in lockstep to strip
link syntax cleanly so callers that pull a plain-text projection
(e.g.ProjectLabel.parse) keep getting just the visible label.ProjectRenderer.inline(...)andProjectRenderer.titleThenBody(...)
now route the project-row title segment through
MarkdownInline.append(...)instead of emitting it as a flat
RichText.style(...)run. End-to-end consequence: a CV row with
label = "[GraphCompose](https://gc) (Java, PDFBox)"renders the
title as a clickable hyperlink and the stack as plain
" (Java, PDFBox)". Labels without inline Markdown render
identically to before.ProjectRenderer.plainInline(...)(the
one-line listing variant) intentionally continues to drop link
syntax viaMarkdownInline.plainText(...)because a clickable
link would not survive the compact formatting context.ProjectLabel.parse(...)now preserves inline Markdown syntax
inside the returnedtitle(the legacy implementation eagerly
flattened**emphasis**and[links](url)viaplainTextand
then split on the last(). The split heuristic now targets a
trailing\s+\([^()]*\)\s*$pattern so a leading
[name](https://...)URL's(...)segment is not mistaken for
the technology-stack delimiter. Callers that only need the
visible-text projection should passtitle()back through
MarkdownInline.plainText(...).- Four new
BusinessThemefactory presets@since 1.6.8:
BusinessTheme.nordic()(Scandinavian minimal — cool whites +
slate-blue accent + generous whitespace, for design-studio
reports and product launch decks),
BusinessTheme.editorial()(warm cream surface + deep ink +
brick-red accent on a serif body, for long-form proposals and
annual reports),
BusinessTheme.cinematic()(inverted dark navy surface with
light text + bright copper accent, for investor pitch decks and
product launch one-pagers), and
BusinessTheme.monochrome()(pure black-on-white with a single
bold yellow accent, for brutalist editorial layouts where
typographic contrast carries the identity). Pure additions —
no change to the existingclassic()/modern()/
executive()presets. japicmp gate against v1.6.7 reports
semver PATCH(compatible additions only).
Build
- Bumped
jackson-bom2.21.3 → 2.21.4 (broken 2.22.0 skipped via
the.github/dependabot.ymlignore entry added in v1.6.7),
logback-classic1.5.32 → 1.5.34 (fixes
CVE-2026-9828 —
deserialization whitelist bypass inHardenedModelInputStream),
central-publishing-maven-plugin0.7.0 → 0.9.0 (0.10.0
blocked by the existing ignore entry; revisit after a focused
release-profile evaluation),japicmp-maven-plugin0.23.1 →
0.26.1, and a handful ofmaven-*-pluginminor/patch bumps
(clean / site / resources / enforcer 3.5.0 → 3.6.3 / surefire
3.5.5 → 3.5.6 / source 3.3.1 → 3.4.0 / gpg 3.2.7 →
3.2.8) (#115,
cherry-picked frommainto aligndevelop).
CI
.github/dependabot.ymlnow pins both ecosystems
(maven,github-actions) totarget-branch: developso future
grouped PRs land on the integration branch instead ofmain.
Closes the divergence root cause behind the v1.6.7-era #111 /
#115 episodes where every Dependabot PR force-split history
between branches and required a cherry-pick to align.
Documentation
- New quickstart guide
Testing your document —
end-to-end recipe (author the document → add a layout
snapshot test → bless the baseline → CI guards the
shape on every PR), with a "when to use which layer" table for
the three protection tiers (smoke / layout snapshot / pixel-level
visual). Complements the existing
layout-snapshot-testing.md
reference: that one is reference-style, the new one is
tutorial-style. README's "What can I do with this?" table row
now links to both.
Web
- New Next.js showcase site under
site/is now the official
GitHub Pages deploy target for v1.6.8 onwards. Fully static
one-page marketing / playground built with Next.js 14 App
Router + TypeScript + Tailwind.next buildemits./out(4
static pages, 99.7 kB first-load JS) and the new
.github/workflows/deploy-site.yml
uploads it to Pages on every push tomainthat touches
site/**. Repo Settings → Pages source must be flipped to
"GitHub Actions" for the workflow to take over from the
legacy branch-based deploy ofdocs/index.html; both files
coexist in the tree for one more cycle as a rollback. - Live code snippets in the Hero / Playground sections mirror
the canonical README hello-world,examples/.../InvoiceFileExample,
andModernProfessional.create()paths, so a visitor copying
any snippet into a fresh Maven project pulled at
io.github.demchaav:graph-compose:1.6.8gets compiling code.
Gallery enumerates the full 16-preset cv/v2 lineup (15
paired cover letters;MinimalUnderlinedships without a
paired letter by design). scripts/cut-release.ps1learns a newUpdate-SiteDepsVersion
step so the Maven / Gradle install snippets in
site/lib/deps.tsflip in lockstep with the README + pom
versions at cut time — no more silent drift between the site
and the real released coordinates. The same release commit
now also stages `site...
GraphCompose v1.6.7
v1.6.7 — Dependency cleanup release
TL;DR — Zero breaking changes. Lighter runtime classpath
(Kotlin stdlib gone, flexmark narrowed to the 3 modules actually
used, jackson-yaml marked optional). Plus one latent bug fix in
DocumentSession.registry().register(...) cache invalidation.
Install
Maven Central (recommended):
<dependency>
<groupId>io.github.demchaav</groupId>
<artifactId>graph-compose</artifactId>
<version>1.6.7</version>
</dependency>Gradle:
implementation("io.github.demchaav:graph-compose:1.6.7")Highlights
- 🪶 Lighter runtime — Kotlin stdlib removed (the library is
Java-first; no production.ktsources existed),flexmark-all
aggregator replaced byflexmark+flexmark-util-ast+
flexmark-util-data(the three modulesMarkDownParseractually
uses),jackson-dataformat-yamlmarked<optional>true</optional>
(mirrors the existingpoi-ooxmlpattern), unused
jackson-module-jsonSchemaand the directsnakeyamldeclaration
dropped. - 🐛 Layout-cache fix —
session.registry().register(...)now
invalidates the layout cache the same way
session.registerNodeDefinition(...)does. Previously, registering
a definition throughregistry()silently left a stale compiled
layout in the cache. - ✅
japicmpverdict —semver PATCH, compatible bug fixvs
the v1.6.6 baseline. The one surface delta isNodeRegistry
becoming non-finalso the session can install an
auto-invalidating subclass.
Migration from v1.6.6
No code changes required for typical usage. Pure-PDF consumers
and JSON-only ConfigLoader callers carry on unchanged.
If you previously got these dependencies transitively through
GraphCompose, declare them explicitly:
| If you transitively depended on… | Add to your build |
|---|---|
| Kotlin stdlib | org.jetbrains.kotlin:kotlin-stdlib-jdk8 |
| Flexmark extensions (tables, footnotes, gfm-strikethrough, …) | the relevant com.vladsch.flexmark:flexmark-ext-* modules |
YAML config loading through ConfigLoader |
com.fasterxml.jackson.dataformat:jackson-dataformat-yaml |
jackson-module-jsonSchema |
com.fasterxml.jackson.module:jackson-module-jsonSchema |
The commons-logging API beyond SLF4J routing |
commons-logging:commons-logging (we exclude it from PDFBox and bridge via jcl-over-slf4j) |
Next
v1.6.8 is in develop — CV v2 migration completion (inline
markdown-link parser for project/education titles) plus polish
follow-ups from the v1.6.7 senior review. v1.7.0 is the
canonical-DSL feature minor (LineBuilder.dashed, inline shapes,
TimelineBuilder, dx shortcuts, recipes docs).
Resources
📦 Maven Central
· 📚 Javadocs
· 📋 Full CHANGELOG
· 🗺️ Roadmap
· 📐 API stability policy
Detailed release notes
v1.6.7 — 2026-06-01
Transitive dependency cleanup. v1.6.7 narrows the runtime
classpath GraphCompose imposes on consumers. The Kotlin standard
library is gone (the codebase is Java-first; no production
.kt sources exist), the flexmark-all aggregator is replaced
with the three modules MarkDownParser actually references,
jackson-dataformat-yaml is marked <optional>true</optional>
(mirroring the existing poi-ooxml pattern — only consumers that
load YAML configs through ConfigLoader need to pull it in),
jackson-module-jsonSchema and the explicit snakeyaml
declaration are dropped as unused, and jcl-over-slf4j is added
explicitly so PDFBox's commons-logging call sites keep routing
through SLF4J after the flexmark narrowing (the bridge was
previously provided transitively via flexmark-all). The cycle
also fixes a latent layout-cache staleness bug on
DocumentSession.registry().register(...) (Track I3): the
registry returned by registry() is now a session-owned wrapper
that invalidates the layout cache on every mutation, matching the
semantics of DocumentSession.registerNodeDefinition(...).
Zero breaking public API changes. The japicmp gate against
the v1.6.6 baseline reports semver PATCH, compatible bug fix —
the one surface delta is NodeRegistry becoming non-final so
DocumentSession can install the auto-invalidating subclass
described above. All existing call sites compile and run
unchanged. The transitive cleanup is a runtime-classpath change,
not a compile-surface change.
Migration from v1.6.6. Consumers that relied on dependencies
flowing transitively through GraphCompose must now declare them
explicitly:
| If you transitively depended on… | Add to your build |
|---|---|
| Kotlin stdlib via GraphCompose | org.jetbrains.kotlin:kotlin-stdlib-jdk8 |
| Flexmark extensions (tables, footnotes, gfm-strikethrough, …) | the relevant com.vladsch.flexmark:flexmark-ext-* modules |
YAML config loading through ConfigLoader |
com.fasterxml.jackson.dataformat:jackson-dataformat-yaml |
jackson-module-jsonSchema |
com.fasterxml.jackson.module:jackson-module-jsonSchema |
The commons-logging API beyond SLF4J routing |
declare commons-logging:commons-logging explicitly (GraphCompose intentionally excludes it from PDFBox and bridges via jcl-over-slf4j) |
No code changes are required for typical usage — pure-PDF
consumers and JSON-only ConfigLoader callers carry on as before.
The next minor with new canonical DSL primitives is v1.7.0
(see ROADMAP.md).
Build
- Dropped the
kotlin-stdlib-jdk8compile dependency, the
kotlin-testtest dependency, and thekotlin-maven-plugin
build extension. GraphCompose is Java-first; no production
Kotlin sources exist, and the runtime now no longer carries
the Kotlin standard library transitively. Consumers that
relied onkotlin-stdlibflowing through GraphCompose must
declare it explicitly. - Replaced the
flexmark-allaggregator dependency with the three
modules actually referenced byMarkDownParser:flexmark
(core parser + AST),flexmark-util-ast(Node / NodeVisitor /
VisitHandler), andflexmark-util-data(MutableDataSet). No
extension modules (tables, footnotes, gfm-strikethrough, etc.)
are used by GraphCompose. Consumers that relied on extensions
flowing through GraphCompose must depend on the relevant
flexmark-ext-*modules explicitly. - Added
jcl-over-slf4jas an explicit compile dependency. PDFBox
3.0.7'sPDDocument.<clinit>callsorg.apache.commons.logging. LogFactorydirectly; we exclude PDFBox's owncommons-logging
artifact to keep one logging facade, and the bridge routes those
calls through SLF4J. Previously the bridge was provided
transitively viaflexmark-all; making it explicit keeps the
classpath reproducible after the flexmark narrowing above. - Marked
jackson-dataformat-yamlas<optional>true</optional>,
mirroring the existingpoi-ooxmlpattern. The only consumer is
ConfigLoader.loadConfigWithEnv(...)when the caller passes a
.yaml/.ymlresource; library consumers that load JSON
configs (or skipConfigLoaderaltogether) no longer pull in the
~1.7 MB SnakeYAML transitive footprint. Applications that load
YAML configs through this helper must now declare
jackson-dataformat-yamlin their own build. - Removed the unused
jackson-module-jsonSchemadependency — no
code path references it. - Removed the explicit
snakeyamldependency declaration and the
snakeyaml.versionproperty. SnakeYAML is now resolved
transitively (andoptional) throughjackson-dataformat-yaml,
which version-aligns it with Jackson's BOM. - Bumped
net.sf.jasperreports:jasperreports6.21.3 → 7.0.7
in the benchmarks module. Benchmarks are a sibling Maven module
consumed only by the manual performance harness — no impact on
library consumers (#111).
Documentation
ConfigLoader.loadConfigWithEnvJavadoc now states the YAML
path requiresjackson-dataformat-yamlon the classpath and
throwsNoClassDefFoundErrorwhen the optional dep is absent.DocumentSession.registry()Javadoc now explains that the
returned registry is a session-owned wrapper whose
register(...)mutates the registry and invalidates the
layout cache, making the two registration entry points
(session.registry().register(...)and
session.registerNodeDefinition(...)) interchangeable.
Fixes
DocumentSession.registry().register(...)now invalidates the
layout cache the same way
DocumentSession.registerNodeDefinition(...)does. Previously,
registering a node definition throughregistry()mutated the
registry in place but left the cachedLayoutGraphpinned to
the previous compile, so a follow-up call torender(...)or
layoutGraph()silently returned the stale graph routed through
the old definition. Implemented by wrapping the session's
NodeRegistryin a private session-owned subclass that funnels
everyregister(...)call throughinvalidate(). (Track I3.)
Internal
NodeRegistryis no longerfinalsoDocumentSessioncan
install a session-owned subclass that auto-invalidates the
layout cache on mutation (see Fixes above). Standalone
NodeRegistryinstances retain their previous behaviour.- Replaced eight residual
org.jetbrains.annotations.NotNull/
@Nullableusages withlombok.NonNull(where the surrounding
file already used Lombok) or removed them entirely (private
methods and test fixtures).org.jetbrains:annotationsis no
longer on the runtime classpath after the Kotlin...
GraphCompose v1.6.6
v1.6.6 — 2026-05-31
First Maven Central release. GraphCompose now ships under
io.github.demchaav:graph-compose:1.6.6 — note the hyphenated
artifactId, chosen for readability ahead of the Central debut. The
release adds publishable sources/javadoc jars, GPG-signed artefacts,
a binary-compatibility gate against v1.6.5, the metadata Maven
Central requires, and a substantial documentation polish for the
maturity / stability / migration story.
Zero breaking changes from v1.6.5. Existing JitPack callers continue
to resolve through the same coordinates (com.github.DemchaAV:GraphCompose:v1.6.5);
existing API surface compiles and runs unchanged (validated by the new
japicmp gate against the v1.6.5 baseline). New: the @Beta
annotation marker, the @since 1.0.0 class-level Javadoc on
entry-point packages, and a curated docs pass (decision guide for
the two template surfaces, examples maturity index, explicit API
stability policy).
Migration from v1.6.5: no code changes required. Swap the
JitPack <dependency> for the Maven Central form
(io.github.demchaav:graph-compose:1.6.6). The legacy JitPack URL
keeps resolving for callers pinned to v1.6.5 and earlier.
Build
- Binary-compatibility gate against v1.6.5 (
japicmpprofile,
Track E1). The newbinary-compatCI job builds the artifact on every
pull request and diffs it againstcom.github.DemchaAV:GraphCompose:v1.6.5
pulled from JitPack. Binary-incompatible modifications to the public
surface fail the build; source-incompatible changes are reported only
(phased policy, will tighten after the 1.6.6 cut). Run locally with
./mvnw -DskipTests -P japicmp verify -pl .; HTML/MD/XML reports
land intarget/japicmp/. JitPack repository is scoped to the
japicmpprofile, so downstream consumers do not inherit it. - Maven Central publish workflow (Track D4). New
.github/workflows/publish.ymlfires
on the samev*tag push that triggers the existing
release.yml. It re-runsmvnw verifyat the tagged commit, imports
the GPG key (Track D2) into the runner keyring, writes the
<server id="central">credentials block into~/.m2/settings.xml
viaactions/setup-java@v5, then invokes
./mvnw -P release -Dgpg.skip=false deploy— the
central-publishing-maven-plugin(Track D3) uploads to Central and
blocks until Sonatype's validator responds. Hyphenated tags
(-rc,-alpha,-beta,-snapshot) are explicitly skipped — those
ship only to JitPack and the GitHub Release pre-release surface.
Aworkflow_dispatchinput lets the maintainer re-publish an
existing tag without re-cutting it if Central had a transient
validator hiccup. The workflow is dormant until four GitHub repo
secrets are wired:MAVEN_GPG_PRIVATE_KEY,MAVEN_GPG_PASSPHRASE,
CENTRAL_USERNAME,CENTRAL_TOKEN. docs/contributing/release-process.mdupdated with the
end-to-end Maven Central runbook (Track D4 docs). New § 2.C
"One-time Maven Central setup (maintainer)" walks through GPG key
generation, keyserver upload, Sonatype account / namespace
verification, Central user-token generation, the four GitHub
secrets, and the release-candidate dry-run strategy. § 2.B
post-release checklist gains a new step 9 for the Central publish
alongside the existing JitPack step.- Hosted Javadocs via
javadoc.io(Track H3). README's
distribution-status note now points callers at
javadoc.io/doc/io.github.demchaav/graph-compose,
which auto-mirrors any artefact published to Maven Central within
minutes — no separate hosting infrastructure required. The note
also pins Maven Central as the going-forward primary distribution
starting v1.6.6 (JitPack stays available alongside for existing
callers). The full Central install snippet ("Central as primary,
JitPack as fallback") lands in the v1.6.6 release-prep PR after the
first Central publish proves the pipeline end-to-end. central-publishing-maven-pluginin thereleaseprofile
(Track D3). Adds Sonatype'scentral-publishing-maven-plugin0.7.0
to the existingreleaseprofile as a packaging extension. Replaces
the legacynexus-staging-maven-plugin+ manual staging-repository
workflow with a singledeploycall. Configuration:
publishingServerId=central(matches the<server id="central">
entry the publish workflow writes fromCENTRAL_USERNAME/
CENTRAL_TOKENsecrets),autoPublish=false(validation gate before
the artefact goes live — flips totrueonce we're confident
post-D4),waitUntil=validated(the build waits for Sonatype's
validator so any rejection surfaces in the workflow run, not a
silent stuck upload). Requires theio.github.demchaavnamespace to
be verified oncentral.sonatype.com(one-time human step via
GitHub auth or DNS TXT record). The plugin loads inert until D4's
workflow provides the credentials.- GPG signing in the
releaseprofile (Track D2). Adds
maven-gpg-plugin3.2.7 to the existingreleaseprofile, binding
to theverifyphase to sign main / sources / javadoc / pom
artefacts — Maven Central rejects unsigned uploads. Off by
default: a new property<gpg.skip>true</gpg.skip>keeps local
mvn -P release packageruns working without a configured GPG key.
The publish workflow (Track D4) flips it explicitly with
-Dgpg.skip=falseonce theMAVEN_GPG_PRIVATE_KEYand
MAVEN_GPG_PASSPHRASEsecrets are wired.gpgArgumentsdeclares
--pinentry-mode loopbackso non-interactive CI runs accept the
passphrase from-Dgpg.passphrase/MAVEN_GPG_PASSPHRASEwithout
needing a TTY forgpg-agent. releaseMaven profile with sources + javadoc jars (Track D1).
Activated with-P release, attaches*-sources.jarand
*-javadoc.jarto thepackagephase via the standard
maven-source-plugin(3.3.1) andmaven-javadoc-plugin(3.12.0)
configurations Maven Central requires. The Javadoc plugin runs with
doclint=noneandfailOnError=falseso Lombok-generated members
and@Internalengine surface don't block a publish; warnings are
surfaced quietly. Defaultmvnw verifystill does not pay the
~30 s of extra packaging — the profile is off by default and turned
on bycut-release.ps1(once Track D3's central-publishing plugin
lands) and the publish workflow (Track D4).- SCM block canonicalised in
pom.xml(Track D1 polish). The
Central metadata validator is strict about the<scm>block:
<connection>now usesscm:git:https://…(HTTPS, not the legacy
git://transport) and<developerConnection>now uses
scm:git:ssh://git@github.com/…(the canonical SSH URL with the
git@user, not the olderssh://github.com:…form). Matches the
shape every Central artefact's POM carries. - New
benchmarks/README.md(Track B1). Honest framing for the
manual benchmark layer ahead of the Maven Central debut: explicitly
positions the harness as a smoke / diff / endurance tool — not a
JMH-grade benchmark — and tells callers when not to use it
(publishable performance claims, architectural decisions,
cross-library comparisons that read too much into a single number).
Documents the file-by-file role of each runner / report tool, the
exact CI smoke invocation, and a "How to read a report" cheat sheet.
Cross-links the planned JMH chain (Track C, B3 → B6 in 1.7.0) so a
reader knows what's coming and how to identify "rigorous"
measurements when they arrive. - Class-level
@since 1.0.0Javadoc on the public entry-point
surface (Track H1). 26 public types in the canonical user-reached
packages (com.demcha.compose.GraphCompose,com.demcha.compose.document.api.{DocumentSession, DocumentPageSize, PageBackgroundFill},
com.demcha.compose.document.dsl.{DocumentDsl, RichText, Transformable}plus all 19 DSL builders)
now carry class-level@since 1.0.0Javadoc tags so callers can see
the introduction version at IDE quick-doc / generated Javadoc time
without trawling CHANGELOG history. New guard test
PublicApiSinceTagCoverageTestsource-scans the three entry-point
roots and fails the build if a new public top-level type lands
without a class-level@sincetag;internal/sub-packages are
excluded by convention (InternalAnnotationCoverageTestcovers those).
Method-level@sincebackfill for the ~380 public methods in these
packages is intentionally out of scope here and tracked separately. maven-enforcer-plugingate (Track E2). Binds three rules to the
validatephase so the build refuses to start when a precondition is
broken:requireJavaVersion(≥ 17 — the declared baseline, catches
accidental JDK 11 / 15 attempts),requireMavenVersion(≥ 3.8.0 —
the oldest version the planned central-publishing pipeline supports),
andrequirePluginVersions(every plugin must declare an explicit
non-LATEST/ non-RELEASE/ non-SNAPSHOTversion — the
generalisation of the PR-7.1 exec-plugin drift lesson).
Default-lifecycle plugins (clean/install/site/resources/
deploy) are now pinned in a new<pluginManagement>block so
requirePluginVersionshas nothing to flag. Minimums and versions
live in<properties>(enforcer.requireMavenVersion,
enforcer.requireJavaVersion,maven.enforcer.plugin.version).- Parallel-session stress test (Track I2). New
DocumentSessionParallelStressTestdrives 32 independent
DocumentSessioninstances on a fixed-size thread pool through 4
iterations and asserts (a) all parallel renders produce a layout-graph
signature byte-equal to the sequential baseline — exercising the
shared font registry, glyph cache, built-in node definitions, and
shape-outline cache for race conditions; (b) every PDF output starts
with the%PDFmagic, is at least 256 bytes, an...
GraphCompose v1.6.5
v1.6.5 — 2026-05-30
Templates v2
- Added the
CenteredHeadlineCV preset to thecv/v2layered
template surface, including its isolated theme tokens, visual
regression baselines, and reusableSubheadline/
SectionHeader.flatSpacedCapswidget support. - Added the Mint Editorial template set: a two-page, two-column
editorial CV presetMintEditorial(centred spaced-caps masthead with
a full-width mint accent rule; sidebar contact / interests / education /
expertise / skill-bars / social beside a profile / experience / awards /
references main column) and its pairedMintEditorialLetter, both on
CvTheme.mintEditorial()and with visual regression baselines. - Added two reusable
cv/v2/widgets:SkillBar(data-driven proficiency
bar — spaced-caps label above a track with a level-positioned marker;
no bar when the level is absent) andIconTextRow(inline icon + text
row, optionally a single click target), withWidgetSmokeTestcoverage. - Added optional proficiency levels to
SkillGroupvia the new
CvSkillrecord andSkillsSection.Builder.leveledGroup(...). Fully
backward-compatible: name-only skills carry no level and every existing
name-based renderer is unaffected. - Added
MintEditorial.Options(and a matchingMintEditorialLetter.Options)
— an additive masthead colour API (accent, rule, name, and an optional
full-width page-1 header band) whose defaults reproduce the stock render
exactly, so the committed look and the parity baselines are unchanged.
Public API
PageBackgroundFillband helpers. AddedtopBand,bottomBand,
band,topBandPoints, andbandPointsfactory methods for full-width
horizontal background bands (top, bottom, or arbitrary vertical offset;
ratio- or point-based), complementing the existing column helpers and
building on the v1.6.5 y-coordinate fix below.
Bug fixes
PageBackgroundFilly-coordinate. A partial-height page-background
fill (heightRatio < 1.0) was painted from the page bottom upward
instead of from theyRatiotop edge the API documents, so a band with
yRatio = 0rendered at the bottom of the page. Fills now convert the
top-down ratios to the PDF bottom-up origin correctly
(y = (1 - yRatio - heightRatio) * pageHeight); full-page and
full-height column fills are unchanged. Adds top-/bottom-/mid-band
regression tests.GraphCompose.document().pageBackgrounds(emptyList())now actually
clears. The builder's Javadoc promised that an explicit empty list
overrides any earlierpageBackground(color)on the same builder, but
the implementation skipped empty lists, sopageBackground(LIGHT_GRAY)
followed bypageBackgrounds(List.of())still emitted the grey
background. The guard is removed; the empty list is now the documented
clear. Adds a regression test.distributeRowSlotWidthsweights / children mismatch. When a row
was constructed with aweightslist whose size did not match the
number of children (only reachable by bypassingRowBuilderand
building aRowNodedirectly), the engine's row distribution code
walked off the end of theweightslist with a raw
IndexOutOfBoundsException. Both row-distribution call sites
(LayoutCompiler#distributeRowSlotWidths,NodeDefinitionSupport#measureRow)
now reject the mismatch with anIllegalArgumentExceptionwhose
message names both sizes and the expected fix.RowNode's canonical
constructor already validated this at construction time; the new
engine guards are defence-in-depth for any path that bypasses it
(e.g. reflection-based deserialization). Adds regression tests for
the canonical-constructor IAE and theRowBuilder.build()ISE.
Build
byte-buddyis now<scope>test</scope>. Mockito already excludes
its transitivebyte-buddyand the project pins a single version in a
standalone dependency; that dependency was missing a scope, so the
published POM advertisedbyte-buddyas a compile dependency even
though no production code references it. Setting<scope>test</scope>
keeps the version pin but keepsbyte-buddyout of consumers' runtime
classpath (mvn dependency:treeshows it only as:test).- CI
exec-maven-pluginversion drift removed. The CI workflow's
three benchmark steps invoked
org.codehaus.mojo:exec-maven-plugin:3.5.0:javadirectly, while
benchmarks/pom.xmlalready declaredexec-maven-pluginat3.6.3
for local runs — a silent version split between CI and local invocations
that grew the surface area to keep aligned. CI now calls the configured
plugin viaexec:java, picking up the pinned3.6.3from
benchmarks/pom.xml. No behaviour change; one fewer hardcoded version
to bump.
v1.6.4 — Boxed Sections parser fix + structured WorkHistory/Education blocks
Bug fix + structured-block patch. Adds two new public Block types —
WorkHistoryBlock and EducationBlock — that let template authors
declare work-history and education entries with explicit (title,
organisation, date, description) / (degree, institution, year,
details) fields instead of relying on the legacy
MultiParagraphBlock pipe-separated string parser. Also closes a
Boxed Sections layout defect that bundled the date and description
into the right-aligned date column for any author-supplied line that
used an em-dash (" — "), en-dash (" – "), or contained
prose-shaped content the parser misread as a date. No public API
break — the sealed Block permit list grows from six to eight,
existing MultiParagraphBlock work-history strings continue to
parse, and the deprecated parser path stays in place for backward
compatibility.
Templates — new structured blocks
WorkHistoryBlock. New public record block carrying a list of
Item(title, organisation, date, description)entries. The
BoxedSectionspreset renders each item as a structured row:
title bold on the left, date right-aligned on the same row,
organisation italic on the next line under the title, and
description as a full-width paragraph beneath. Other presets fall
back to a single concatenated paragraph per item. Authors who use
WorkHistoryBlockbypass the legacy
BoxedSections#parseWorkEntryheuristic parser entirely.EducationBlock. New public record block carrying a list of
Item(degree, institution, year, details)entries. Renders with
the same structured layout asWorkHistoryBlock(degree bold
left, year right, institution italic, details paragraph) so
Education & Certifications sections visually match Professional
Experience.- Sample data migrated.
ExampleDataFactory.sampleCvSpecV2now
usesWorkHistoryBlockfor Professional Experience and
EducationBlockfor Education & Certifications. The legacy
MultiParagraphBlockpattern remains supported and is exercised
byPresetLayoutSnapshotTest/PresetVisualParityTestto lock
the backward-compat path.
Templates — parser robustness (legacy path)
parseWorkEntryaccepts em-dash and en-dash. Used to split
the post-pipe segment on ASCII" - "only; now tries" — ",
" – ", and" - "in order, mirroringsplitHeading. Authors
who typed"*2024-Present* — Led reusable document flows."saw
the whole tail collapse into the date column — this no longer
happens.parseWorkEntryrejects prose dressed up as a date. The
looselooksLikeDatecheck accepted any string containing a
year and a hyphen anywhere, which caused education lines like
"... | 2019. First-class honours. Specialisation ..."to
parse as work entries (the hyphen inside"First-class"was
enough to satisfy the heuristic). Parser now rejects post-pipe
segments that contain sentence-ending punctuation (.,:,
;) when no explicit date / description separator was found,
letting these lines fall back to plain paragraph rendering.
Marked@Deprecatedwith a@deprecatedJavadoc pointing
callers toWorkHistoryBlock/EducationBlock.parseProjectItempicks up the same em-dash / en-dash /
ASCII separator set so future Project items typed with em-dash
don't regress into "title only" rendering.
Tests
BlockTest.blockSealingPermitsAllEightVariantsupdated for the
two new permitted block types.PresetVisualGalleryTest.sampleSpecmigrated to
WorkHistoryBlockso the visible "primary example" exercises the
new structured shape.PresetLayoutSnapshotTestintentionally retained on
MultiParagraphBlockto lock the legacy parser's behaviour.
v1.6.3 — Tight PDF link rects + preserved whitespace + Projects layout fix
Bug fix patch. Closes two independent hyperlink clickable-area
defects that surfaced on CV gallery presets and made the LinkedIn /
GitHub contact rows hijack each other's clicks (paragraph-level
link path) or drift past their visible text (span-level link path
through multi-space separators). No public API change — engine,
DSL, themes, templates, and backend records all stay
source-compatible with v1.6.2.
Engine
- Paragraph-level link annotations now hug rendered text.
PdfFixedLayoutBackendused to emit a paragraph'slinkOptions
as a single rectangle covering the entire fragment box
(fragment.x()+fragment.width()), ignoringTextAlign.RIGHT
/TextAlign.CENTER. Stacked right-aligned contact paragraphs
(e.g. one per LinkedIn / GitHub icon row in Timeline Minimal /
Sidebar Portrait / Monogram Sidebar) therefore produced
full-column-wide rects that overlapped the empty alignment gap of
neighbouring rows — hovering over GitHub clicked the LinkedIn row.
The backend now emits one per-line rect tight toline.width()
positioned at the alignment-awarelineX, matching how
inline-span links already worked. Span-level link emission, table
/ shape / barcode payload links, and bookmark anchoring are
unchanged. - Glyph sanitizer preserves all author whitespace.
PdfFont.sanitizeForRenderused to collapse any run of consecutive
spaces into a single space, both for whitespace-only tokens (the
" "halves of a" | "separator) and for inter-word gaps
in spaced-caps strings (spacedUpper("ARTEM DEMCHYSHYN")produces
"A R T E M D E M C H Y S H Y N"with deliberate triple-spaces
between words). The collapse shrank the rendered glyph stream
under measurement, drifting inline-link rectangles ~8pt per
" | "separator past their visible labels and visually
merging spaced-caps titles back into a single run ("A R T E M D E M C H Y S H Y N"— no word boundary). The sanitizer no longer
collapses adjacent spaces; newlines / NBSP / non-tab control
characters still resolve to a single space each, but author
whitespace is now preserved verbatim so wrap geometry,
link-rectangle emission, andshowText(...)all see the same
string. Layout snapshot baselines for five CV presets and one
nested-list document widened to reflect the recovered whitespace —
the deliberate visual change is the bug fix.
Templates
- Boxed Sections projects render as title + indented description.
The "Projects" module now renders each bullet-list or
IndentedBlockitem as two stacked paragraphs — bullet plus bold
project name (with an optional tech-stack chunk in parentheses) on
the first line, then a hanging-indented description below aligned
to the project name (not the bullet). The previous single-line
rendering ran the project name and description together. Bullet
marker, hanging-indent, and surrounding modules are unchanged.
Example data inExampleDataFactory.sampleCvSpecV2and
PresetVisualGalleryTestnow ships tech-stack chunks ("Java 21, PDFBox, Maven, JMH") so the gallery PDFs reflect the new layout.
Tests
- New regression in
PdfFixedLayoutBackendFeaturesTest—
shouldTightlyHugRightAlignedParagraphLinkRectangles— stacks
three right-aligned link paragraphs and asserts each clickable
rect hugs its rendered label width (≤ 150pt), sits flush against
the inner right margin, and does not overlap the Y-band of
neighbouring rows. - New regression in
PdfFixedLayoutBackendFeaturesTest—
shouldKeepCenteredInlineLinkRectanglesAlignedAcrossMultiSpaceSeparators
— renders a centered contact line built with" | "separators
and asserts the three resulting link rectangles preserve
left-to-right order with non-overlapping X ranges and a sane
per-separator gap (5..40pt), pinning the bug where collapsed
whitespace pushed later rects past the line. - New regression in
PdfFontSanitizerTest—
sanitizeForRender_preservesWhitespaceOnlyTokensVerbatim— pins
the whitespace-only short-circuit so render width stays in
lockstep withgetTextWidthfor tokenised contact-line
separators.
v1.6.2 — Sorry, the PDF was crashing on emoji
v1.6.2 — Sorry, the PDF was crashing on emoji
Patch release. Closes four engine bugs I found while building yet
another CV template and assuming "well, this one definitely works."
It did not.
Zero breaking changes. If you're on v1.6.1, upgrading is safe.
Honest.
What's fixed
🔡 Any Unicode glyph no longer kills the document.
Before: a single ● in a paragraph, watermark, or footer would
make PDFBox throw IllegalArgumentException deep inside showText
and the whole render went up in smoke. I was convinced this was a
known limitation. It was a bug. PdfFont.sanitizeForRender already
existed in the codebase, nobody was calling it. Now everybody is.
Emoji, arrows, copyright marks — substituted to ? with one WARN
per unique (font, codepoint). No crashes. No surprises.
📏 842pt no longer "too large" for A4.
DocumentPageSize.A4.height() is 841.88977pt. Exactly. Per the
spec. If you are a regular human being who writes 842 because A4
is 842pt tall (as everyone learned) — you used to get
requires outer height 842.0 but page capacity is 841.88977. A
0.11pt overflow. That's 0.04 mm. The capacity check now has a 0.5pt
tolerance — invisible to the eye, but enough to stop fainting at
round-trip rounding.
🎨 Row inside LayerStack is now legal.
Want a dark top band on your CV with a sidebar+main row underneath?
Obviously through a LayerStack (layer 0 = band, layer 1 = row of
two columns). Obviously the validator rejected this, because it
couldn't tell a parent-row-band from a parent-layer-rectangle. It
can now. Row inside a layer works. Row inside another row still
does not — that one is a real composition conflict.
🛠️ Exception messages now tell you what to do.
Before: cannot contain a nested horizontal row; use a section column instead. Sounds like a lecture from your compiler. After:
Wrap the inner row in a LayerStack layer (allowed since v1.6.2), or stack horizontal content as sections inside a vertical column.
Meaning: try this thing. Breakthrough, I know.
Tests
828+ existing tests stay green, plus:
PdfFontSanitizerTest— 8 sanitizer casesLayerStackRowCompositionTest— 3 positive + 1 negativePaginationEdgeCaseTest— 2 new boundary cases- Three visual demo PDFs under
target/visual-tests/:
glyph-fallback/,page-capacity/,layer-stack/ - Bonus:
DevelopTestscratch class for manual experimentation
Meta
- Maintainer email in
pom.xmlis fixed. Nobody was emailing the
old one, which was convenient. If you ever wanted to reach me —
it now actually goes somewhere: demchishynartem@gmail.com. - README links to the companion experiment
graphcompose-ai-flow
— where I'm exploring AI-assisted document authoring on top of
GraphCompose. Independent codebase, separate lifecycle, broken
things welcome. Check it out if that sounds fun.
Install
<dependency>
<groupId>com.github.DemchaAV</groupId>
<artifactId>GraphCompose</artifactId>
<version>v1.6.2</version>
</dependency>implementation("com.github.demchaav:GraphCompose:v1.6.2")What's next — v1.7
DX helpers on top of an engine that no longer crashes: heading
levels (h1/h2/h3), addFieldRow(label, value),
page.twoColumn(...), addHeadingBar(...), inline shape runs
(ratingDots, badge). All additive. Plan tracked privately;
follow develop.
Full diff: v1.6.1...v1.6.2
GraphCompose v1.6.1 — Java 17 baseline + dependency refresh
For users
GraphCompose v1.6.1 is a maintenance + compatibility patch. Drops the Java 21 source/target baseline to Java 17+ so the library can ship into older enterprise stacks without a fork, and refreshes test/build dependencies (incl. CVE pass + ByteBuddy/Mockito update so Mockito works on JDK 25).
No public API change — engine, DSL, themes, templates, and backend records all stay source-compatible with v1.6.0; existing v1.6.0 callers compile and behave unchanged.
Co-developed with external contributor @jottinger (#8, #10).
Toolchain
- Java 17 baseline.
<maven.compiler.release>flips from21to17acrosspom.xml,examples/pom.xml, andbenchmarks/pom.xml. Engine source loses the Java 21–only constructs (switch-with-type-patterns, switch-with-deconstruction,List.getFirst(),Thread.threadId()) in favour of Java 17–compatible forms. CI runs against Temurin JDK 17 / 21 / 25 in matrix. - Dependency refresh + CVE pass. Bumps Jackson
2.20.1 → 2.21.3, Logback1.5.18 → 1.5.32, Lombok1.18.38 → 1.18.46, POI5.4.0 → 5.5.1, SnakeYAML2.4 → 2.6, AssertJ3.27.3 → 3.27.6, JUnit5.12.2 → 5.14.4, Mockito5.20.0 → 5.23.0. Adds explicit ByteBuddy1.18.7so Mockito works on the Java 25+ access rules. Maven plugin bumps:maven-compiler-plugin 3.13 → 3.15,maven-surefire-plugin 3.2.5 → 3.5.5,exec-maven-plugin 3.5 → 3.6.2.
Verified
- CI green on Temurin JDK 17 / 21 / 25 in parallel matrix.
- 819 / 0 / 0 / 0 canonical test suite green on every JDK.
- 26 runnable examples regenerate cleanly.
- Engine source has zero remaining Java 21–only constructs (
getFirst,getLast,threadId, switch type patterns, switch deconstruction).
Quick start
Maven:
<repositories>
<repository><id>jitpack.io</id><url>https://jitpack.io</url></repository>
</repositories>
<dependency>
<groupId>com.github.DemchaAV</groupId>
<artifactId>GraphCompose</artifactId>
<version>v1.6.1</version>
</dependency>Gradle (Kotlin DSL):
repositories { maven("https://jitpack.io") }
dependencies { implementation("com.github.demchaav:GraphCompose:v1.6.1") }Looking ahead
Maven Central distribution (#7) remains queued for the upcoming v1.7.0, alongside the JMH benchmark migration. v1.6.1 stays on JitPack.
Links
- Live showcase — install snippet, every example with a generated PDF preview, template gallery, feature examples.
- Templates v2 landing — 14 CV + 14 paired cover-letter presets.
- Migration v1.5 → v1.6 — for callers still on v1.5 (engine source-compatible to v1.6.x).
- CHANGELOG.md — full technical changelog.
Author intent, not coordinates.
GraphCompose v1.6.0 — expressive release
For users
v1.6.0 — "expressive" — makes GraphCompose more expressive without breaking your engine code:
- Nested lists are now easier to compose —
ListBuilder.addItem(label, Consumer)opens a child scope, and a per-depth marker cascade (•→◦→▪→·) handles the visual hierarchy. Mixed flat / nested authoring preserves source order. - Table cells can hold any composable node —
DocumentTableCell.node(...)accepts paragraphs, nested lists, sub-tables, layer stacks. Two-pass measurement preserves the row-by-row pagination contract. - Canvas layers support pixel-precise
(x, y)placement —CanvasLayerNodeis the controlled free-canvas primitive for when you do need coordinates, withClipPolicy.CLIP_BOUNDSclipping and atomic pagination. - Templates v2 ships 14 CV presets and 14 paired cover-letter presets, theme-driven via
BusinessTheme, with one-linercreate(theme)factories. Inline markdown, active hyperlinks, slot-based multi-column layouts. The Templates v2 visual pipeline is hardened with pixel-diff parity checks against v1 reference renders to catch visual regressions on every build. - Architecture hardening —
@InternalAPI stability marker on engine internals, publicPdfFragmentRenderHandlerSPI for custom render handlers,DocumentRenderingExceptionwrapping the convenience render path sobuildPdf/writePdf/toPdfBytesno longer declarethrows Exception, documented thread-safety contract.
Engine source-compatibility with v1.5 is preserved for unmodified callers. Every public record that grew a new field ships back-compat constructors that default the new value, so v1.5 callers compile and behave unchanged.
Breaking change
Templates v2 replaces the legacy CV / cover-letter template classes. Legacy classes (CvTemplateV1, NordicCleanCvTemplate, MonogramSidebarCvTemplate, …) are deleted, not deprecated. Anyone constructing those classes must switch to the matching v2 preset's create(BusinessTheme) factory. The full v1 → v2 mapping (every old class → its v2 replacement, with before/after code) is documented in docs/migration-v1-5-to-v1-6.md.
Quick start
Maven:
<repositories>
<repository><id>jitpack.io</id><url>https://jitpack.io</url></repository>
</repositories>
<dependency>
<groupId>com.github.DemchaAV</groupId>
<artifactId>GraphCompose</artifactId>
<version>v1.6.0</version>
</dependency>Gradle (Kotlin DSL):
repositories { maven("https://jitpack.io") }
dependencies { implementation("com.github.demchaav:GraphCompose:v1.6.0") }Distribution status — currently published via JitPack. Maven Central is planned for v1.7 (tracking issue #7).
What's verified
- 819 / 0 / 0 / 0 — full canonical test suite green via
mvnw verify. - 26 runnable examples regenerate cleanly via
GenerateAllExamplesand ship as a CI artifact on every build. - 28 layout-snapshot baselines + 29 pixel-diff visual baselines cover every CV / cover-letter preset. The parity gate runs on every CI build with a calibrated
mismatchedPixelBudgetfor cross-platform PDFBox font drift.
Links
- Live showcase — install snippet, every example with a generated PDF preview, template gallery, feature examples.
- Templates v2 landing — preset gallery, four-layer architecture explanation, quick-start.
- Migration v1.5 → v1.6 — v1 → v2 template upgrade table.
- CHANGELOG.md — full technical changelog, including architecture-hardening details, package-level changes, and ADRs 0011-0014.
Author intent, not coordinates.