@@ -673,8 +675,8 @@
- com.github.DemchaAV
- GraphCompose
+ io.github.demchaav
+ graph-compose-core
${japicmp.baseline}
@@ -686,51 +688,42 @@
true
- false
+ true
false
false
true
true
- com.demcha.compose.document.layout.payloads
-
- com.demcha.compose.engine.measurement.TextMeasurementSystem
-
- com.demcha.compose.ConfigLoader
+ com.demcha.compose.engine.**
+ com.demcha.compose.document.layout.**
+ @com.demcha.compose.document.api.Internal
diff --git a/core/src/test/java/com/demcha/documentation/VersionConsistencyGuardTest.java b/core/src/test/java/com/demcha/documentation/VersionConsistencyGuardTest.java
index 9db9e0de..870ed0d8 100644
--- a/core/src/test/java/com/demcha/documentation/VersionConsistencyGuardTest.java
+++ b/core/src/test/java/com/demcha/documentation/VersionConsistencyGuardTest.java
@@ -12,7 +12,6 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
@@ -34,15 +33,14 @@
* which is the drift class that previously let the benchmarks module run
* against the previous release.
*
- * The snippet checks accept the version as matching either
- * the current {@code pom.xml} version or the version named in
- * the top {@code CHANGELOG.md} {@code Planned} entry. This makes the
- * forward-looking Maven Central snippet (which has to advertise the
- * about-to-ship version so users copy a coord that will resolve once the tag
- * lands) compatible with the pre-cut window where {@code pom.xml} still carries
- * the previous release version. {@code cut-release.ps1} bumps both pom and
- * snippet to the same target in the release commit, after which the two paths
- * converge and the test continues to pass.
+ *
The install-snippet checks require the snippets to advertise a version that
+ * a user can actually resolve. During a normal {@code -SNAPSHOT} development cycle
+ * that is the latest published release (the topmost dated
+ * {@code CHANGELOG.md} entry) — not the in-development {@code -SNAPSHOT} and not a
+ * forward-looking {@code Planned} version. In the release commit itself
+ * {@code cut-release.ps1} bumps the pom off {@code -SNAPSHOT} and rewrites the
+ * snippets to the new release version, so once the pom carries a concrete release
+ * version the snippets must equal it. See {@link #acceptableTargets()}.
*/
class VersionConsistencyGuardTest {
@@ -227,10 +225,10 @@ void readmeInstallSnippetsMatchTheProjectVersion() throws Exception {
String gradleSnippetVersion = firstMatchingGroup(readme, INSTALL_SNIPPET_PATTERNS_README_GRADLE);
assertThat(mavenSnippetVersion)
- .describedAs("README Maven install snippet must reference the current pom or CHANGELOG Planned version (one of %s)", targets)
+ .describedAs("README Maven install snippet must reference the latest published release, or the release version in a release commit (one of %s)", targets)
.isIn(targets);
assertThat(gradleSnippetVersion)
- .describedAs("README Gradle install snippet must reference the current pom or CHANGELOG Planned version (one of %s)", targets)
+ .describedAs("README Gradle install snippet must reference the latest published release, or the release version in a release commit (one of %s)", targets)
.isIn(targets);
}
@@ -247,19 +245,19 @@ void showcaseSiteVersionMatchesTheProjectVersion() throws Exception {
String site = Files.readString(PROJECT_ROOT.resolve("web/index.html"));
assertThat(firstGroup(site, "\"softwareVersion\":\\s*\"v?([0-9][^\"]*)\""))
- .describedAs("web/index.html JSON-LD softwareVersion must equal the current pom or planned version (one of %s)", targets)
+ .describedAs("web/index.html JSON-LD softwareVersion must equal the latest published release, or the release version in a release commit (one of %s)", targets)
.isIn(targets);
// Match up to the delimiter (space before `·`) rather than
// digits-and-dots only, so a pre-release version like 2.0.0-rc.1 is
// captured too — consistent with the JSON-LD and snippet patterns.
assertThat(firstGroup(site, "v([0-9][^\\s&]*)\\s*·\\s*MIT"))
- .describedAs("web/index.html hero version badge must equal the current pom or planned version (one of %s)", targets)
+ .describedAs("web/index.html hero version badge must equal the latest published release, or the release version in a release commit (one of %s)", targets)
.isIn(targets);
assertThat(firstMatchingGroup(site, INSTALL_SNIPPET_PATTERNS_SHOWCASE_MAVEN))
- .describedAs("web/index.html Maven install snippet must equal the current pom or planned version (one of %s)", targets)
+ .describedAs("web/index.html Maven install snippet must equal the latest published release, or the release version in a release commit (one of %s)", targets)
.isIn(targets);
assertThat(firstMatchingGroup(site, INSTALL_SNIPPET_PATTERNS_SHOWCASE_GRADLE))
- .describedAs("web/index.html Gradle install snippet must equal the current pom or planned version (one of %s)", targets)
+ .describedAs("web/index.html Gradle install snippet must equal the latest published release, or the release version in a release commit (one of %s)", targets)
.isIn(targets);
}
@@ -287,10 +285,10 @@ void moduleReadmeInstallSnippetsMatchTheProjectVersion() throws Exception {
String readme = Files.readString(PROJECT_ROOT.resolve(module.getKey()));
String artifact = Pattern.quote(module.getValue());
assertThat(firstGroup(readme, "" + artifact + "\\s*v?([0-9][^<]*)"))
- .describedAs("%s Maven install snippet must equal the current pom or planned version (one of %s)", module.getKey(), targets)
+ .describedAs("%s Maven install snippet must equal the latest published release, or the release version in a release commit (one of %s)", module.getKey(), targets)
.isIn(targets);
assertThat(firstGroup(readme, "io\\.github\\.demchaav:" + artifact + ":v?([0-9][\\w.\\-]*)"))
- .describedAs("%s Gradle install snippet must equal the current pom or planned version (one of %s)", module.getKey(), targets)
+ .describedAs("%s Gradle install snippet must equal the latest published release, or the release version in a release commit (one of %s)", module.getKey(), targets)
.isIn(targets);
}
}
@@ -343,23 +341,38 @@ void companionReadmeInstallSnippetsMatchTheirPomVersions() throws Exception {
};
/**
- * Returns the set of versions that any install snippet may legitimately
- * advertise: the current {@code pom.xml} version always, plus the version
- * named in the top {@code CHANGELOG.md} {@code Planned} entry if one
- * exists. The Planned entry covers the pre-cut window where {@code pom.xml}
- * still carries the previous release version while the README + showcase
- * already advertise the about-to-ship version.
+ * Returns the set of versions an install snippet may legitimately advertise.
+ *
+ *
During a normal {@code -SNAPSHOT} development cycle this is only
+ * the latest published release — the version actually resolvable on
+ * Maven Central — so a user who copies a snippet always gets a coordinate that
+ * resolves today, never the in-development {@code -SNAPSHOT} nor a
+ * forward-looking {@code Planned} version. In the release commit itself
+ * {@code cut-release.ps1} bumps the pom off {@code -SNAPSHOT} to the new
+ * release version and rewrites the snippets to match, so once the pom is a
+ * concrete release version the snippets must equal it.
*/
private Set acceptableTargets() throws Exception {
- Set targets = new LinkedHashSet<>();
- targets.add(effectiveVersion(PROJECT_ROOT.resolve("core/pom.xml")));
+ String pomVersion = effectiveVersion(PROJECT_ROOT.resolve("core/pom.xml"));
+ if (pomVersion.endsWith("-SNAPSHOT")) {
+ return Set.of(latestPublishedRelease());
+ }
+ return Set.of(pomVersion);
+ }
+
+ /**
+ * The latest published release: the topmost dated {@code CHANGELOG.md} entry
+ * ({@code ## vX.Y.Z — YYYY-MM-DD}). {@code find()} returns the newest such
+ * entry; a {@code — Planned} entry is skipped because it carries no date.
+ */
+ private String latestPublishedRelease() throws Exception {
String changelog = Files.readString(PROJECT_ROOT.resolve("CHANGELOG.md"));
- Matcher planned = Pattern.compile("^## v([0-9][^ \\n]*)\\s*[\\u2014\\-]\\s*Planned\\b", Pattern.MULTILINE)
+ Matcher released = Pattern.compile("^## v([0-9][^ \\n]*)\\s*[\\u2014\\-]\\s*\\d{4}-\\d{2}-\\d{2}", Pattern.MULTILINE)
.matcher(changelog);
- if (planned.find()) {
- targets.add(planned.group(1));
- }
- return targets;
+ assertThat(released.find())
+ .describedAs("CHANGELOG.md must contain a dated release entry (## vX.Y.Z — YYYY-MM-DD) to anchor the install snippets")
+ .isTrue();
+ return released.group(1);
}
/**
diff --git a/coverage/pom.xml b/coverage/pom.xml
index eebd7c9d..fb9b6ae2 100644
--- a/coverage/pom.xml
+++ b/coverage/pom.xml
@@ -26,7 +26,7 @@
io.github.demchaav
graph-compose-build
- 2.0.0
+ 2.0.1-SNAPSHOT
../pom.xml
diff --git a/docs/api-stability.md b/docs/api-stability.md
index 65d207f4..be0bf5cc 100644
--- a/docs/api-stability.md
+++ b/docs/api-stability.md
@@ -183,6 +183,32 @@ window starts, and its `Status` flips to `deprecated 1.x`.
| `DocumentSession.pageMargins(List)` / `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](migration/v2.0.0-modules.md). | [ADR 0016](adr/0016-multi-module-packaging.md) | — |
+### Binary-compatibility enforcement
+
+The Stable-tier promise (§ 1 — no binary breaks outside a major release) is enforced
+mechanically by [japicmp](https://siom79.github.io/japicmp/), run in a `japicmp` Maven
+profile on the engine module during `verify`.
+
+- **Baseline:** the published `graph-compose-core` on Maven Central, pinned by the
+ `japicmp.baseline` property in `core/pom.xml`. It is the current major's **floor** —
+ `2.0.0` for the whole 2.x line — and advances 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 the `2.0.0` public surface, not
+ merely with the last minor.
+- **What fails the build:** any binary-incompatible change to the public surface
+ against the baseline — a removed or less-accessible public method/field/type, a
+ changed signature, and so on. `@Internal` packages (`com.demcha.compose.engine.*`,
+ `com.demcha.compose.document.layout.*` and its render-handoff payload records) are
+ excluded; they carry no compatibility promise (§ 1). 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.
+- **Activity window:** the gate compares the working version against the baseline, so
+ it is a no-op only when the two are equal — the `2.0.0` release commit itself — and
+ active for every `-SNAPSHOT` development cycle across the 2.x line that follows.
+
+During the 2.0 major transition the gate ran report-only (the major intentionally
+broke 1.x binary compatibility); it enforces from the `2.0.0` baseline forward.
+
---
## 4. Tier mapping per package
diff --git a/examples/pom.xml b/examples/pom.xml
index 3e65182a..3dea9e92 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -7,7 +7,7 @@
io.github.demchaav
graph-compose-build
- 2.0.0
+ 2.0.1-SNAPSHOT
../pom.xml
diff --git a/pom.xml b/pom.xml
index b70a9141..9b29c825 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
io.github.demchaav
graph-compose-build
- 2.0.0
+ 2.0.1-SNAPSHOT
pom
GraphCompose Build Aggregator
diff --git a/qa/pom.xml b/qa/pom.xml
index 3b09b0cb..48c54c24 100644
--- a/qa/pom.xml
+++ b/qa/pom.xml
@@ -22,7 +22,7 @@
io.github.demchaav
graph-compose-build
- 2.0.0
+ 2.0.1-SNAPSHOT
../pom.xml
diff --git a/render-docx/pom.xml b/render-docx/pom.xml
index 8440791b..5f01e93e 100644
--- a/render-docx/pom.xml
+++ b/render-docx/pom.xml
@@ -19,7 +19,7 @@
-->
io.github.demchaav
graph-compose-render-docx
- 2.0.0
+ 2.0.1-SNAPSHOT
GraphCompose Render — DOCX
Semantic DOCX export backend for GraphCompose, backed by Apache POI.
diff --git a/render-pdf/pom.xml b/render-pdf/pom.xml
index 9fd99459..4947d6b7 100644
--- a/render-pdf/pom.xml
+++ b/render-pdf/pom.xml
@@ -25,7 +25,7 @@
-->
io.github.demchaav
graph-compose-render-pdf
- 2.0.0
+ 2.0.1-SNAPSHOT
GraphCompose Render — PDF
The PDFBox-backed PDF render backend for GraphCompose.
diff --git a/render-pptx/pom.xml b/render-pptx/pom.xml
index 89eb144c..1e27a770 100644
--- a/render-pptx/pom.xml
+++ b/render-pptx/pom.xml
@@ -19,7 +19,7 @@
-->
io.github.demchaav
graph-compose-render-pptx
- 2.0.0
+ 2.0.1-SNAPSHOT
GraphCompose Render — PPTX
Semantic PPTX export backend for GraphCompose (slide-safe semantic node validation).
diff --git a/scripts/japicmp-verify-excludes.sh b/scripts/japicmp-verify-excludes.sh
new file mode 100755
index 00000000..aa1f2f05
--- /dev/null
+++ b/scripts/japicmp-verify-excludes.sh
@@ -0,0 +1,64 @@
+#!/usr/bin/env bash
+#
+# Controlled verification that the japicmp binary-compatibility gate enforces the
+# Stable public surface and excludes the Internal surface exactly as
+# docs/api-stability.md defines it (§ 1 tiers, § 4 package map, "Binary-
+# compatibility enforcement"). Run it after changing the japicmp in
+# core/pom.xml, or after moving a type between the Stable and Internal tiers.
+#
+# It applies one controlled break at a time, runs the gate, checks the outcome,
+# and ALWAYS reverts the source (even on interrupt) via a trap. Requires a clean
+# working tree for the two touched files. The engine version must be off the
+# japicmp baseline (a -SNAPSHOT dev version) or the gate short-circuits.
+#
+# Expected: a Stable break FAILS the gate; an Internal break does NOT.
+#
+set -u
+
+HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "$HERE/.." && pwd)"
+cd "$REPO_ROOT"
+MVNW="$REPO_ROOT/mvnw"
+
+# A Stable public method (document.style is a Stable package) and an Internal
+# public method (document.layout is @Internal). Both are compile-safe to reduce to
+# package-private: rgba has no main callers, compile() has no cross-package caller.
+STABLE_FILE="core/src/main/java/com/demcha/compose/document/style/DocumentColor.java"
+INTERNAL_FILE="core/src/main/java/com/demcha/compose/document/layout/LayoutCompiler.java"
+
+LOGDIR="$REPO_ROOT/target/japicmp-verify"
+mkdir -p "$LOGDIR"
+MVN_ARGS=(-B -ntp clean -P japicmp -Dmaven.test.skip=true -Djacoco.skip=true verify -pl :graph-compose-core)
+
+revert() { git checkout -- "$STABLE_FILE" "$INTERNAL_FILE" 2>/dev/null || true; }
+trap revert EXIT
+
+fail=0
+
+echo "== [1/2] Stable break must FAIL the gate =="
+perl -0pi -e 's/ public static DocumentColor rgba/ static DocumentColor rgba/' "$STABLE_FILE"
+if "$MVNW" "${MVN_ARGS[@]}" > "$LOGDIR/stable.log" 2>&1; then
+ echo " FAIL: the gate PASSED a Stable-surface break (DocumentColor.rgba)"; fail=1
+elif grep -q "DocumentColor.rgba.*METHOD_LESS_ACCESSIBLE" "$LOGDIR/stable.log"; then
+ echo " OK: the gate failed the build (DocumentColor.rgba METHOD_LESS_ACCESSIBLE)"
+else
+ echo " FAIL: the gate failed, but not on the rgba break — see $LOGDIR/stable.log"; fail=1
+fi
+git checkout -- "$STABLE_FILE"
+
+echo "== [2/2] Internal break must NOT fail the gate =="
+perl -0pi -e 's/ public LayoutGraph compile\(/ LayoutGraph compile(/' "$INTERNAL_FILE"
+if "$MVNW" "${MVN_ARGS[@]}" > "$LOGDIR/internal.log" 2>&1; then
+ echo " OK: the gate passed (LayoutCompiler.compile is excluded via document.layout.**)"
+else
+ echo " FAIL: the gate FAILED an Internal-surface break — see $LOGDIR/internal.log"; fail=1
+fi
+git checkout -- "$INTERNAL_FILE"
+
+echo ""
+if [ "$fail" = "0" ]; then
+ echo "VERIFY PASS — Stable enforced, Internal excluded."
+else
+ echo "VERIFY FAIL — see the logs above."
+fi
+exit "$fail"
diff --git a/templates/pom.xml b/templates/pom.xml
index e5341e90..34028cd3 100644
--- a/templates/pom.xml
+++ b/templates/pom.xml
@@ -17,7 +17,7 @@
-->
io.github.demchaav
graph-compose-templates
- 2.0.0
+ 2.0.1-SNAPSHOT
GraphCompose Templates
Built-in CV, cover-letter, invoice, and proposal document templates for GraphCompose.
diff --git a/testing/pom.xml b/testing/pom.xml
index 6c43ab49..3fa18625 100644
--- a/testing/pom.xml
+++ b/testing/pom.xml
@@ -19,7 +19,7 @@
-->
io.github.demchaav
graph-compose-testing
- 2.0.0
+ 2.0.1-SNAPSHOT
GraphCompose Testing
Consumer testing support for GraphCompose: layout-snapshot assertions and PDF visual regression.
diff --git a/wrapper/pom.xml b/wrapper/pom.xml
index 24b75897..2e9978d9 100644
--- a/wrapper/pom.xml
+++ b/wrapper/pom.xml
@@ -22,7 +22,7 @@
-->
io.github.demchaav
graph-compose
- 2.0.0
+ 2.0.1-SNAPSHOT
GraphCompose
The graph-compose coordinate: a drop-in aggregator over graph-compose-core for a PDF-capable install.