Skip to content

Commit cf62726

Browse files
committed
docs(api): add API stability policy (G3)
Wires up Track G3 from the v1.6.5→1.7 readiness taskboard — the third and final onboarding-clarity PR for the 1.6.6 Maven Central debut (after G1 which-template-system #86 and G2 examples-maturity #87). ADR-0003 defines the @internal annotation mechanism, but there has been no user-facing policy that tells callers: - What "Stable" actually promises across patch / minor / major. - Where Extension SPI sits and what the @beta annotation will mean once it lands. - That sealed hierarchies in Stable types can grow new permits in minor releases — and what that means for exhaustive switch code. - How long a @deprecated element stays before it goes away. - Which package belongs to which tier without reading Javadoc per element. docs/api-stability.md is the user-facing policy: §1 Stability tiers — Stable / Extension SPI / Internal / Experimental with marker, scope, breaking-change rules, and the explicit promise each tier carries. §2 Sealed hierarchies — additive permits must degrade gracefully without default-branch failures; CHANGELOG names every new public permit; @internal sealed types are explicitly outside the policy. §3 Deprecation window — ≥ 1 minor release with @deprecated for Stable / Extension SPI; removed in next major (Stable) or next minor with CHANGELOG callout (Extension SPI). Internal / Experimental: no window required. §4 Per-package tier — lookup table covering com.demcha.compose.* plus the legacy packages. §5 Anti-policy — what we explicitly do NOT promise (pixel- stable PDFs, bit-stable bytes, sealed-permit exhaustiveness in Stable across minor releases). §6 References — cross-links to ADR-0003 / 0004 / 0011 / 0015 and the architecture guard tests that pin the policy in code. CanonicalSurfaceGuardTest.PUBLIC_MARKDOWN_ALLOWLIST gains a new entry for docs/api-stability.md (same audit-log precedent as the G1 which-template-system page and the v1.5→v1.6 migration log) — the §4 tier table and §3 deprecation example name retired legacy types on purpose. Cross-links from §1 and §4 point at G1's which-template-system page for the template-surface migration path; the two pages reference each other naturally (templates page covers template surfaces; this page covers everything else). Verification: - ./mvnw -B -ntp test -pl . -Dtest='CanonicalSurfaceGuardTest,DocumentationCoverageTest,DocumentationExamplesTest,VersionConsistencyGuardTest' -> 30 tests, 0 failures, 0 errors (~19s) CHANGELOG entry added to v1.6.6 — Planned under the existing ### Documentation subsection alongside G1 and G2.
1 parent d464658 commit cf62726

3 files changed

Lines changed: 237 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,22 @@ JitPack continue to resolve through the existing coordinates.
6666
maturity legend introduces the five tiers and links to
6767
`docs/templates/which-template-system.md` for the V1 → V2 path that
6868
the **Legacy** tier points at.
69+
- **New API stability policy: [`docs/api-stability.md`](docs/api-stability.md)**
70+
(Track G3). User-facing companion to
71+
[ADR-0003](docs/adr/0003-api-stability-and-internal-marker.md): pins
72+
the four stability tiers (**Stable**, **Extension SPI**, **Internal**,
73+
**Experimental**) with what each one promises in patch / minor /
74+
major releases, the sealed-hierarchy permit-list policy (additive
75+
variants must degrade gracefully without `default`-branch failures),
76+
the deprecation window (≥ 1 minor release with `@Deprecated`, removed
77+
in next major), a per-package tier-lookup table for the canonical
78+
surface plus the legacy packages headed for 2.0 removal, and an
79+
"anti-policy" section (no pixel-stable PDFs, no bit-stable artefact
80+
bytes, no sealed-permit exhaustiveness across minor releases for
81+
Stable hierarchies). `CanonicalSurfaceGuardTest` allowlist extended
82+
so the page can name `com.demcha.templates.*` / `com.demcha.compose.v2.*`
83+
and the legacy `pdf(Path)` factory in the package-tier and
84+
deprecation-example sections.
6985

7086
## v1.6.5 — 2026-05-30
7187

docs/api-stability.md

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
# API Stability Policy
2+
3+
GraphCompose follows **Semantic Versioning** (major.minor.patch). This
4+
page is the user-facing contract for which parts of the public surface
5+
that promise covers, what breaking changes are allowed in each release
6+
type, and how sealed hierarchies, deprecations, and unannounced
7+
internal changes are handled.
8+
9+
The mechanism side of the same decision — how `@Internal` is wired up
10+
and which guard tests enforce it — lives in
11+
[ADR-0003](adr/0003-api-stability-and-internal-marker.md). This page is
12+
the **policy** that ADR's mechanism enforces.
13+
14+
---
15+
16+
## 1. Stability tiers
17+
18+
Every public class, method, field, and annotation that lives under
19+
`com.demcha.compose.*` falls into exactly one of four tiers. The tier
20+
is signalled by the package it lives in (with one exception, `@Internal`,
21+
which can appear on individual elements too) and by an explicit
22+
annotation marker.
23+
24+
| Tier | Marker | Used for | Breaking changes allowed in |
25+
|---|---|---|---|
26+
| **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 `BusinessTheme` and `CvTheme` factories, the recommended template presets in `cv.v2.*` and `coverletter.v2.*`. | **Major releases only.** |
27+
| **Extension SPI** | `@Beta` _(annotation arriving in a near-term 1.6.x release; this row already describes the policy that will apply)_ | 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. |
28+
| **Internal** | [`@Internal`](../src/main/java/com/demcha/compose/document/api/Internal.java) (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. | **Any release.** No deprecation window, no CHANGELOG entry required. |
29+
| **Experimental** | `@Beta` _(same annotation as Extension SPI; the distinction lives in the docstring)_ | 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. |
30+
31+
> The repo's [`@Internal`](../src/main/java/com/demcha/compose/document/api/Internal.java)
32+
> annotation already exists and is enforced by
33+
> [`InternalAnnotationCoverageTest`](../src/test/java/com/demcha/documentation/InternalAnnotationCoverageTest.java)
34+
> and `InternalAnnotationDocumentationTest`. The `@Beta` annotation is
35+
> still pending — until it lands, Extension SPI and Experimental tiers
36+
> are signalled in Javadoc prose only; this page describes the policy
37+
> the annotation will encode once it ships.
38+
39+
### What each tier promises
40+
41+
- **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.
42+
- **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 `@Deprecated` for at least one minor release first, and the CHANGELOG entry calls out the migration explicitly.
43+
- **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 `@Internal` type, you opted out of the stability contract — please open an issue so a stable wrapper can be designed.
44+
- **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.
45+
46+
---
47+
48+
## 2. Sealed hierarchy policy
49+
50+
GraphCompose uses sealed interfaces in several places to keep visitor
51+
code exhaustive. The public ones — the ones this policy actually covers —
52+
are:
53+
54+
- [`Block`](../src/main/java/com/demcha/compose/document/templates/blocks/Block.java) (Stable)
55+
- [`CvSection`](../src/main/java/com/demcha/compose/document/templates/cv/v2/data/CvSection.java) (Stable)
56+
- [`InlineRun`](../src/main/java/com/demcha/compose/document/node/InlineRun.java) (Stable)
57+
- [`ShapeOutline`](../src/main/java/com/demcha/compose/document/style/ShapeOutline.java) (Stable)
58+
- [`TemplateModuleBlock`](../src/main/java/com/demcha/compose/document/templates/support/common/TemplateModuleBlock.java) (Extension SPI)
59+
60+
Sealed types under `@Internal` packages — `ParagraphSpan` and
61+
`PlacementContext` — are outside this policy by definition; their permit
62+
list can change in any release without notice.
63+
64+
A `sealed interface X permits A, B, C` carries a stronger contract than
65+
a regular interface: every implementation is known to the compiler, so
66+
a `switch (block)` over the permits list can be exhaustive.
67+
**Adding a new permit is therefore a breaking change** for any caller
68+
that switches on the sealed type without a `default` branch — even
69+
though it's purely additive at the source level.
70+
71+
### Policy
72+
73+
1. **Stable sealed hierarchies are additive in minor releases only when
74+
the new variant carries a sensible default rendering for callers
75+
that did not switch on it.** Concretely: if a caller pattern-matches
76+
on `Block` and hits a `NewlyAddedBlock` it didn't expect, the
77+
default rendering must visually degrade gracefully — typically by
78+
delegating to the closest stable variant (often `ParagraphBlock`)
79+
rather than throwing.
80+
2. **The CHANGELOG entry for the minor release names the new permit
81+
explicitly** under `### Public API` so callers know to audit their
82+
visitor code. Example wording, from the v1.6.4 cut:
83+
> Added two new public Block types — `WorkHistoryBlock` and
84+
> `EducationBlock` — that let template authors declare work-history
85+
> and education entries with explicit fields. The sealed `Block`
86+
> permit list grows from six to eight; existing `MultiParagraphBlock`
87+
> work-history strings continue to parse.
88+
3. **Internal sealed hierarchies have no permit-list policy.** The
89+
compiler enforces exhaustiveness for engine code; the public contract
90+
doesn't surface them at all.
91+
4. **Removing a permit is a major-version event** for any tier other
92+
than `@Internal`.
93+
94+
The same policy applies to sealed *classes* (records and class
95+
hierarchies). The mechanism is identical.
96+
97+
---
98+
99+
## 3. Deprecation window
100+
101+
A Stable API element marked `@Deprecated` is removed only in a major
102+
release, and only after the deprecation has been in effect for at least
103+
one full minor release.
104+
105+
### Rules
106+
107+
| Tier | Minimum deprecation window | Removed in |
108+
|---|---|---|
109+
| **Stable** | ≥ 1 minor release with `@Deprecated`. | Major. |
110+
| **Extension SPI** | ≥ 1 minor release with `@Deprecated`. | Next minor that calls out the migration in CHANGELOG `### Public API`. |
111+
| **Internal** | None required. | Any. |
112+
| **Experimental** | None required. | Any minor. |
113+
114+
### What a deprecation must include
115+
116+
Every `@Deprecated` element ships with a Javadoc note pointing to its
117+
replacement. The format is:
118+
119+
```java
120+
/**
121+
* @deprecated Use {@link com.demcha.compose.GraphCompose#document(java.nio.file.Path)} instead.
122+
* The migration is the same shape — pass the output Path to
123+
* {@code document(...)} rather than to the legacy
124+
* {@code pdf(...)} factory.
125+
* @since (deprecated in) 1.6.0; removed in 2.0
126+
*/
127+
@Deprecated(forRemoval = true, since = "1.6.0")
128+
public static PdfBuilder pdf(Path outputFile) { ... }
129+
```
130+
131+
If a migration target exists, link it. If the deprecation is "this
132+
will simply go away in 2.0 and there is no replacement because the
133+
problem itself moved," say so in prose.
134+
135+
### Currently slated for removal in 2.0
136+
137+
See [`docs/templates/which-template-system.md` § 4](templates/which-template-system.md#4-deprecation-inventory--1x--20)
138+
for the full deprecation inventory.
139+
140+
---
141+
142+
## 4. Tier mapping per package
143+
144+
A quick lookup so callers can classify an import without reading
145+
Javadoc per element.
146+
147+
| Package | Tier | Notes |
148+
|---|---|---|
149+
| `com.demcha.compose` (the `GraphCompose` factory class) | **Stable** | The single entry point. |
150+
| `com.demcha.compose.document.api` | **Stable** | `DocumentSession`, `DocumentBuilder`, `PageBackgroundFill`, and the `@Internal` marker itself live here. |
151+
| `com.demcha.compose.document.dsl` | **Stable** | All builder types (`RowBuilder`, `SectionBuilder`, `ParagraphBuilder`, etc.). |
152+
| `com.demcha.compose.document.node` | **Stable** | Node records (`RowNode`, `SectionNode`, `ParagraphNode`, ...). Sealed where relevant — see § 2. |
153+
| `com.demcha.compose.document.style` | **Stable** | `DocumentColor`, `DocumentInsets`, `DocumentTextStyle`, `DocumentTransform`, ... |
154+
| `com.demcha.compose.document.templates.cv.v2.*` | **Stable** | Layered CV presets, `CvDocument`, `CvTheme`. Recommended template surface. |
155+
| `com.demcha.compose.document.templates.coverletter.v2.*` | **Stable** | Layered cover-letter presets. |
156+
| `com.demcha.compose.document.templates.builtins` | **Stable** | `InvoiceTemplateV2`, `ProposalTemplateV2`, `BusinessTheme`. |
157+
| `com.demcha.compose.document.templates.cv.presets.*` | **Stable but Supported** | The "classic" v1.6 rebuild surface. See [`which-template-system.md`](templates/which-template-system.md). Supported through 1.x; removed in 2.0. |
158+
| `com.demcha.compose.document.templates.support.common` | **Extension SPI** | Helpers template authors build new presets against. `@Beta` arrives in Track H2. |
159+
| `com.demcha.compose.document.layout.*` | **Internal** | Marked `@Internal` at the package level. Engine surface. |
160+
| `com.demcha.compose.engine.*` | **Internal** | Engine surface; not part of the public contract regardless of `public` keyword. |
161+
| `com.demcha.templates.*` | **Legacy** | Pre-rebuild surface; removed in 2.0. See [`which-template-system.md`](templates/which-template-system.md). |
162+
| `com.demcha.compose.v2.*` | **Legacy** | Pre-rebuild engine-direct surface; removed in 2.0. |
163+
164+
---
165+
166+
## 5. What we don't promise (anti-policy)
167+
168+
- **Pixel-stable PDF output across patch releases.** The layout engine
169+
preserves *structural* invariants (page count, fragment ordering,
170+
cell-content order) under semver, but pixel-exact rendering can
171+
shift by a few sub-pixels when PDFBox bumps, font metrics change, or
172+
a kerning fix lands. Layout regression tests (see
173+
`LayoutSnapshotRegressionExample` in the examples README) capture
174+
structure, not pixels; visual regression tests (`*VisualRegressionTest`)
175+
ship with calibrated `mismatchedPixelBudget` values rather than zero.
176+
- **Bit-stable artefact bytes.** PDFs include creation timestamps,
177+
resource ordering hashes, and other metadata that can vary even when
178+
output is visually identical. Compare semantically, not by file hash.
179+
- **Internal package shape across releases.** See § 1, tier Internal.
180+
- **Sealed hierarchy permits' exhaustiveness across minor releases for
181+
Stable hierarchies.** See § 2. Switching on a sealed `Block` without
182+
a `default` branch *will* fail to compile cleanly on the next minor
183+
release that adds a new permit — by design.
184+
185+
---
186+
187+
## 6. References
188+
189+
- [ADR-0003 — API stability boundary and the `@Internal` marker](adr/0003-api-stability-and-internal-marker.md)
190+
— the mechanism side of this policy.
191+
- [ADR-0004 — PDF fragment render handler SPI is public](adr/0004-pdf-handler-spi-extension.md)
192+
— a worked example of opening an Extension SPI seam.
193+
- [ADR-0011 — Templates v2 architecture](adr/0011-templates-v2-architecture.md)
194+
and [ADR-0015 — Layered template architecture](adr/0015-layered-template-architecture.md)
195+
— the architectural justification for the `classic` / `layered`
196+
template tiers in § 4.
197+
- [`docs/templates/which-template-system.md`](templates/which-template-system.md)
198+
— the recommended-vs-legacy decision guide for template surfaces;
199+
this stability policy lives one level up and covers all packages,
200+
not just templates.
201+
- [`InternalAnnotationCoverageTest`](../src/test/java/com/demcha/documentation/InternalAnnotationCoverageTest.java)
202+
and [`InternalAnnotationDocumentationTest`](../src/test/java/com/demcha/compose/document/api/InternalAnnotationDocumentationTest.java)
203+
— the architecture guards that fail the build if the package-level
204+
`@Internal` marker disappears from `document.layout` or the
205+
annotation's contract drifts from this policy.
206+
207+
---
208+
209+
*This page is maintained in lockstep with the public surface. When a
210+
new public package lands, a sealed hierarchy gains a permit, or a
211+
deprecation crosses its window, update §1 (tier matrix), §2 (sealed
212+
policy if relevant), §3 (deprecation table), and §4 (package tier
213+
lookup) in the same commit.*

src/test/java/com/demcha/documentation/CanonicalSurfaceGuardTest.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,14 @@ class CanonicalSurfaceGuardTest {
5555
// so callers can identify legacy imports in their own code and
5656
// see the canonical-DSL replacement. Same purpose as the
5757
// migration log above.
58-
"docs/templates/which-template-system.md");
58+
"docs/templates/which-template-system.md",
59+
// User-facing API stability policy. The package-tier lookup
60+
// table names com.demcha.templates.* and com.demcha.compose.v2.*
61+
// explicitly so callers can classify any import as Stable /
62+
// Extension SPI / Internal / Legacy. The deprecation example
63+
// also shows the legacy `pdf(Path)` factory paired with its
64+
// canonical-DSL replacement. Same audit-log rationale.
65+
"docs/api-stability.md");
5966
private static final List<String> FORBIDDEN_PUBLIC_AUTHORING_IMPORTS = List.of(
6067
"import com.demcha.compose.engine.");
6168

0 commit comments

Comments
 (0)