From f9b216e7846a2e62b65bb9d21f984d817f06a067 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sat, 25 Jul 2026 00:10:51 +0100 Subject: [PATCH] docs(architecture): retarget the engine documentation to 2.x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The architecture pages still described the Entity/ECS engine removed in 2.0 and a PPTX backend that "is roadmap for v1.6+". A reader following them reached for `RenderStream`, `RenderPassSession`, `TextComponent`, `EntityManager`, `ParentComponent`, `EntityBounds`, `ParentContainerUpdater`, `LayoutTraversalContext`, `ComputedPosition`, `Breakable` and the packages `engine.core` / `engine.pagination` — none of which exist on any live branch. The release runbook called itself the 1.x checklist and pointed every command at `aggregator/pom.xml` and `-pl .`, neither of which is the 2.x layout. Rewrite the stale sections against the code as it is. `overview.md` drops the ECS responsibilities section, names both fixed-layout backends in the pipeline and the package list, and gains a PPTX leg in the diagram. That diagram also had the DOCX edge in the wrong place: `DocxSemanticBackend.export` takes a `DocumentGraph`, so it branches off the semantic tree, not off the resolved layout — which is exactly why it cannot reproduce fixed-layout geometry. `package-map.md` rebuilds the engine table from the live package list (`components` / `debug` / `font` / `measurement` / `render` / `text`), splits the render-docx and render-pptx row that claimed both were semantic exporters, and adds the `document.backend.fixed.pptx` row. `lifecycle.md` loses its v1.2 title, gains the PPTX render path, and states the chrome coverage per backend instead of calling metadata and watermarks PDF-only. `pagination-ordering.md` describes the mechanism that actually runs — a single sequential compile pass whose emission order is render order — and its debugging steps now name the live pagination log events and `layoutSnapshot()`. The runbook becomes the 2.x checklist: the whole-reactor gate, the 13-pom lockstep list taken from the script's own array, the real staging allow-list including the per-module READMEs, and a branch-flow section describing the standing roles rather than the completed 2.0 transition. It also records two traps that cost time this cycle: a gate command ending in a pipe reports the pipe's status and hides `BUILD FAILURE`, and after a version bump every goal resolving train modules from `~/.m2` needs an `install` first. Verified: full reactor `clean verify` green (1508 tests, 0 failures); no dead engine identifier remains anywhere under docs/architecture. --- docs/architecture/canonical-legacy-parity.md | 6 +- docs/architecture/lifecycle.md | 22 ++-- docs/architecture/overview.md | 107 ++++++++++--------- docs/architecture/package-map.md | 10 +- docs/architecture/pagination-ordering.md | 71 +++++++----- docs/contributing/release-process.md | 82 +++++++------- 6 files changed, 170 insertions(+), 128 deletions(-) diff --git a/docs/architecture/canonical-legacy-parity.md b/docs/architecture/canonical-legacy-parity.md index 0db4fdfc6..fcd7644c3 100644 --- a/docs/architecture/canonical-legacy-parity.md +++ b/docs/architecture/canonical-legacy-parity.md @@ -8,9 +8,9 @@ Public application code should start with: `GraphCompose.document(...) -> DocumentSession -> DocumentDsl -> layout graph -> PdfFixedLayoutBackend` -`EntityManager` and raw engine builders remain internal, test-support, or -compatibility concerns. New authoring features should be added through -`com.demcha.compose.document.*`, not through low-level entity assembly. +The `com.demcha.compose.engine.*` foundation remains internal. New authoring +features should be added through `com.demcha.compose.document.*`, not by +reaching into engine internals. ## Status Legend diff --git a/docs/architecture/lifecycle.md b/docs/architecture/lifecycle.md index 11d77ec80..7fc129a2a 100644 --- a/docs/architecture/lifecycle.md +++ b/docs/architecture/lifecycle.md @@ -1,6 +1,6 @@ # Canonical Document Lifecycle -GraphCompose v1.2 follows the canonical session-first lifecycle: +GraphCompose follows a session-first lifecycle: ```text GraphCompose.document(...) @@ -9,7 +9,7 @@ GraphCompose.document(...) -> semantic nodes -> layout graph -> layout snapshot or fixed backend render - -> PDF stream/bytes/file + -> PDF or PPTX stream/bytes/file ``` ```mermaid @@ -41,10 +41,12 @@ for layout, pagination, diagnostics, and rendering. `DocumentSession` is mutable and not thread-safe. Create one session per document/request. -Advanced PDF-only options such as metadata, protection, watermark, headers, -and footers are configured through `PdfFixedLayoutBackend.builder()`. The -session convenience PDF methods expose only the common document-level options, -including guide-line overlays. +Backend-level tuning is configured through the backend builder — +`PdfFixedLayoutBackend.builder()` for PDF-specific options such as protection, +and `PptxFixedLayoutBackend.builder()` for PPTX-specific ones such as +deterministic output and the clip raster fallback. The backend-neutral chrome +(metadata, watermark, headers/footers) is set on the session itself and reaches +whichever backend renders, subject to the coverage noted in §5. ## 2. Authoring @@ -78,9 +80,9 @@ snapshot. Pagination happens during layout. Semantic nodes define whether they are atomic or splittable. Long paragraphs/lists can split into fragments; atomic blocks move to the next page when needed. -The lower-level ECS engine still has pagination helpers under `com.demcha.compose.engine.pagination` for internal tests, diagnostics, and backend/tooling work. +Pagination lives entirely in `LayoutCompiler` and the `NodeDefinition` split contracts under `com.demcha.compose.document.layout`. A node opts into keep-together or keep-with-next behaviour through its semantic flags; the compiler resolves the page break and emits the resulting `PlacedFragment` records. -## 5. Render +## 5. Render (PDF / PPTX) `DocumentSession.writePdf(OutputStream)` renders the resolved layout graph through `PdfFixedLayoutBackend` and writes the PDF to a caller-owned stream without closing it. This is the preferred server path because the session does not keep a PDF byte-array cache. @@ -94,6 +96,10 @@ The canonical PDF backend: - dispatches placed fragments to payload handlers - applies bookmarks, links, guide lines, metadata, watermarks, headers/footers, and protection +`writePptx(OutputStream)`, `toPptxBytes()` and `buildPptx(...)` are the PowerPoint counterparts, resolved through the `"pptx"` provider when `graph-compose-render-pptx` is on the classpath. `PptxFixedLayoutBackend` consumes the **same** resolved layout graph — one page becomes one identically-sized slide, and fragments land at the same coordinates — then emits native POI XSLF shapes rather than a rasterised page. + +The chrome options are backend-neutral, but coverage differs. Watermarks and repeating headers/footers apply to both backends; metadata applies to both, mapping onto OPC core properties for PPTX where the producer field has no equivalent. Protection, viewer preferences and the debug guide-line overlays are PDF concepts that the PPTX backend ignores with a one-time warning. See the [backend capability matrix](./backend-capability-matrix.md) for the per-capability breakdown. + ## 6. Close Always close the session, normally with try-with-resources. Closing releases measurement resources and clears request-local text measurement caches. It does not require consumers to manage PDFBox objects directly. diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index e7f9439f5..67a8b5e9e 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -20,13 +20,19 @@ flowchart TD A["Application code — GraphCompose.document(...)"] --> B["DocumentSession + DocumentDsl
(document.api · document.dsl)"] B --> C["Semantic DocumentNode tree
(document.node) — renderer-neutral"] C --> D["LayoutCompiler + NodeRegistry
(document.layout) → LayoutFragments"] + C -->|"export(...)"| H["DocxSemanticBackend — Apache POI
(document.backend.semantic) — reads DocumentGraph"] D --> E["Shared engine foundation — @Internal
(engine.*): measure → paginate → place → order"] - E --> F{"Active backend"} + E --> F{"Fixed-layout backend"} F -->|PDF| G["PdfFixedLayoutBackend
(document.backend.fixed.pdf + engine.render.pdf)"] - F -->|DOCX| H["DocxSemanticBackend — Apache POI
(document.backend.semantic)"] + F -->|PPTX| P["PptxFixedLayoutBackend — POI XSLF
(document.backend.fixed.pptx) — @Beta"] E -.->|"layoutSnapshot()"| I["Deterministic layout snapshot
(regression tests — no bytes rendered)"] ``` +The two fixed-layout backends branch from the **same** resolved graph, which +is why page and slide geometry match by construction. The semantic DOCX +exporter branches earlier, straight off the `DocumentGraph`: it never sees a +`LayoutGraph`, which is why it cannot reproduce fixed-layout geometry. + The PDF path deliberately spans **two** packages: the canonical backend `document.backend.fixed.pdf` owns PDFBox lifecycle and option translation, then dispatches resolved fragments to the engine render handlers under @@ -48,8 +54,10 @@ Concretely: placement, and render ordering against those prepared fragments. 5. the active backend turns the resolved `LayoutGraph` / `PlacedFragment` stream into output bytes — `PdfFixedLayoutBackend` - for PDF, `DocxSemanticBackend` for DOCX, future PPTX backend - skeleton in place. + for PDF and `PptxFixedLayoutBackend` for PowerPoint, both consuming + the same resolved graph, so page and slide geometry match by + construction. `DocxSemanticBackend` takes the other route: it + consumes the semantic node tree directly, without the layout graph. That separation is the core project concept. Public code describes document intent, layout resolves geometry, renderers only draw already @@ -105,11 +113,19 @@ need to reach below it. does not need to touch it. - **`document.backend.fixed.pdf`** — the canonical PDF backend (`PdfFixedLayoutBackend`, fragment render handlers, option - translators). The only place PDFBox imports are allowed outside the - engine foundation. -- **`document.backend.semantic`** — semantic exporters - (`DocxSemanticBackend` based on Apache POI; `PptxSemanticBackend` - manifest skeleton). + translators), shipped in **graph-compose-render-pdf**. The only place + PDFBox imports are allowed outside the engine foundation. +- **`document.backend.fixed.pptx`** — the fixed-layout PowerPoint + backend (`PptxFixedLayoutBackend`, `PptxFragmentRenderHandler` and its + handler set), shipped in **graph-compose-render-pptx**. Consumes the + same resolved `LayoutGraph` as the PDF backend — one page becomes one + identically-sized slide. Marked `@Beta` at the package level; see the + [backend capability matrix](./backend-capability-matrix.md) for + per-capability fidelity. +- **`document.backend.semantic`** — semantic exporters that bypass the + layout graph (`DocxSemanticBackend`, Apache POI; and `PptxSemanticBackend`, + a slide-safe node-graph manifest that predates the fixed-layout backend + and is not what `buildPptx(...)` uses). ## Template layer (`com.demcha.compose.document.templates.*`) @@ -165,26 +181,11 @@ objects are resolved before their parent containers so parent finalized. See [pagination-ordering.md](./pagination-ordering.md) for the detailed rationale and the failure modes that motivated it. -The engine materializes one deterministic hierarchy snapshot per -layout pass: parent links from `ParentComponent`, sibling order from -`Entity.children`, roots / layers / depth metadata rebuilt every pass. -Layout, pagination, snapshot extraction, and render backends all -agree on the same tree semantics. - -### Entity / ECS responsibilities (engine-internal) - -`Entity` is intentionally a thin ECS-style identity object. It owns: - -- stable identity -- the component map -- canonical child order through `Entity.children` -- a cached render marker reference for fast `hasRender()` checks - -Layout-specific math and pagination mutation live in dedicated -helpers — `EntityBounds` for geometry reads, -`ParentContainerUpdater` for parent-container size and page-shift -propagation. Deprecated helper methods on `Entity` are migration -shims, not extension points. +The compiler materializes one deterministic result per layout pass: +`LayoutCompiler` prepares each semantic node into a `PreparedNode`, +paginates it, and emits `PlacedFragment` records into a `LayoutGraph`. +Layout, pagination, snapshot extraction, and render backends all read +that one resolved graph, so they cannot disagree about geometry. ### Semantic modules @@ -219,26 +220,22 @@ rows it merges. These rules apply to engine and backend contributors. Application code should not need any of them. -- engine builders and layout helpers consume an engine-level - `TextMeasurementSystem` instead of reaching through the active - renderer -- render marker components identify *what* needs to be rendered; - *how* it is drawn lives in renderer-owned handler packages such as - the `PdfFragmentRenderHandler` implementations under - `document.backend.fixed.pdf.handlers` -- `RenderStream` acts as a session factory, not as a per-entity - content-stream opener -- `RenderPassSession` is the shared seam for page lifetime and - page-surface reuse — it must stay free of PDFBox and backend - package imports -- the PDF entity path dispatches through registered render handlers; - there is no backend-specific render fallback path - -Fixed leaf primitives (such as `TextComponent` and `BlockText`) -follow the same engine contract: they materialize as regular -entities with render/content/layout components, rely on normal -`ContentSize` / `Padding` / `Margin` / `Placement`, and do not -introduce a separate layout subsystem. +- layout helpers consume an engine-level `TextMeasurementSystem` + instead of reaching through the active renderer, so measurement is + backend-neutral and the same widths produce the layout graph that + every backend then draws +- a `PlacedFragment`'s payload identifies *what* needs to be rendered; + *how* it is drawn lives in renderer-owned handler packages — the + `PdfFragmentRenderHandler` implementations under + `document.backend.fixed.pdf.handlers` and the + `PptxFragmentRenderHandler` set under + `document.backend.fixed.pptx.handlers` +- each fixed-layout backend owns its own render-pass session and page + surface lifetime; that seam stays free of backend-library imports so a + new backend does not have to touch engine code +- fragment dispatch goes through registered handlers only; there is no + backend-specific render fallback path, and an unhandled payload fails + with `UnsupportedNodeCapabilityException` rather than drawing nothing ## Current package roots @@ -276,8 +273,16 @@ last: `ShapeContainerNode` clip and `DocumentTransform` rotation/scale fall back to inline content with a one-time capability warning. Authors who need clipped or rotated output must export to PDF. -- The PPTX skeleton lives behind `PptxSemanticBackend`; richer slide - layout is roadmap for v1.6+. +- The PPTX backend (`PptxFixedLayoutBackend`, Apache POI XSLF) renders + the same resolved `LayoutGraph` as PDF into an editable deck — one + page per identically-sized slide, native shapes rather than pictures. + It ships `@Beta` in 2.1.0: usable for production decks, with the API + shape still open to change in a minor. Clipped composites fall back to + a rasterised island (switchable via `clipRasterFallback(false)`), and + `renderToImages` is unsupported — the per-capability breakdown is the + [backend capability matrix](./backend-capability-matrix.md). + The older `PptxSemanticBackend` manifest remains in the module but is + not on the `buildPptx(...)` path. - New backends should add their own rendering system, render-pass session, text measurement system, and handler set without changing engine builders such as tables or template data models. The shared diff --git a/docs/architecture/package-map.md b/docs/architecture/package-map.md index 2cac1be6d..d684c8b7b 100644 --- a/docs/architecture/package-map.md +++ b/docs/architecture/package-map.md @@ -23,7 +23,8 @@ ships them differs. | --- | --- | | `graph-compose-core` | The lean engine: `com.demcha.compose`, the canonical `document.*` authoring surface (`api` / `dsl` / `node` / `style` / `table` / `snapshot`), `document.showcase` (`FontShowcase`), the `document.backend.fixed` SPI seam, the public `document.backend.fixed.pdf.options` records, `document.layout`, `font.*`, and the internal `engine.*` foundation. | | `graph-compose-render-pdf` | The PDFBox backend: `document.backend.fixed.pdf.**` (the `PdfFixedLayoutBackend` impl + handlers) and the `engine.render.pdf.**` render tree. Registers the PDF `FixedLayoutBackendProvider` / `FontMetricsProvider`. | -| `graph-compose-render-docx` / `graph-compose-render-pptx` | The POI semantic exporters — `document.backend.semantic.docx` / `.pptx`. | +| `graph-compose-render-pptx` | The POI XSLF backend: `document.backend.fixed.pptx.**` (the `PptxFixedLayoutBackend` impl + handlers), registering the `"pptx"` `FixedLayoutBackendProvider`. Also carries the older `document.backend.semantic.pptx` manifest exporter. Depends on `graph-compose-render-pdf` for shared font measurement and the clip raster pass. | +| `graph-compose-render-docx` | The POI semantic exporter — `document.backend.semantic.docx`. | | `graph-compose-templates` | The built-in preset families — `document.templates.**`. | | `graph-compose-testing` | Consumer test support — `com.demcha.compose.testing.**`. | | `graph-compose` | Back-compat wrapper: an empty jar over `graph-compose-core` + `graph-compose-render-pdf`. | @@ -54,6 +55,7 @@ per-package artifact. | `com.demcha.compose.document.layout` (`@Internal` at package level) | Semantic layout compiler, node definitions, fragments, split/measure contracts, and layout graph. | `NodeDefinition` is `@Beta` — Extension SPI for custom node types. New node behavior must be deterministic and covered by layout or render tests. | | `com.demcha.compose.document.backend.fixed` | Backend-neutral fixed-layout rendering contract. | Keep it independent from PDFBox and semantic template data. | | `com.demcha.compose.document.backend.fixed.pdf` | Canonical fixed-layout PDF backend, fragment handlers, PDF-specific options, and PDF-backed measurement resources. | Keep PDFBox lifecycle internal; normal callers should use `DocumentSession` and default PDF convenience methods. | +| `com.demcha.compose.document.backend.fixed.pptx` (`@Beta` at package level) | Fixed-layout PowerPoint backend — `PptxFixedLayoutBackend`, its provider, deterministic writer, embedded-font and clip support, plus the `.handlers` fragment handler set. Consumes the same resolved `LayoutGraph` as the PDF backend. | Keep POI XSLF lifecycle internal. Any capability change must update [backend-capability-matrix.md](./backend-capability-matrix.md) in the same commit. | | `com.demcha.compose.document.dsl.internal` | Internal helpers for public DSL builders such as semantic name normalization and builder callback application. | Do not expose these helpers in examples; move reusable authoring concepts to public builder classes instead. | | `com.demcha.compose.document.backend.semantic` | Experimental semantic export contracts for non-fixed outputs. | Keep exporters separate from PDF fixed-layout rendering. | | `com.demcha.compose.document.debug` | Snapshot/debug adapters for canonical layout graph inspection. | Debug output should be deterministic and safe for tests. | @@ -62,9 +64,9 @@ per-package artifact. | Package | Responsibility | Extension rule | | --- | --- | --- | -| `com.demcha.compose.engine.components.*` | Low-level ECS components, content payloads, style values, geometry, layout, and render markers. | Use only for engine primitives; public document authoring should go through `DocumentDsl` and semantic nodes. | -| `com.demcha.compose.engine.core` | Entity manager, canvas, traversal context, and base ECS system contracts. | Keep core thin; put stage-specific logic in layout, pagination, measurement, or render packages. | -| `com.demcha.compose.engine.pagination` | Pagination markers and helpers (`Breakable`, `ParentContainerUpdater`, `Offset`). | Maintain child-first ordering and page-shift propagation rules. | +| `com.demcha.compose.engine.components.*` | Engine content payloads and their caches — decoded image data and intrinsic sizes, barcode payloads, and the shared bounded LRU cache behind them. | Use only for engine primitives; public document authoring should go through `DocumentDsl` and semantic nodes. | +| `com.demcha.compose.engine.debug` | Layout snapshot value types (`LayoutSnapshot`, `LayoutNodeSnapshot`, `LayoutCanvasSnapshot`, `LayoutInsetsSnapshot`) behind `DocumentSession.layoutSnapshot()`. | Snapshot output must stay deterministic — it is a regression-test contract. | +| `com.demcha.compose.engine.font` | Backend-neutral font abstraction: `Font`, `FontBase`, `FontLineMetrics`. | Backends implement the seam; the catalogue and lookup live in the public `com.demcha.compose.font`. | | `com.demcha.compose.engine.measurement` | Text measurement contracts and font-backed implementations. | Builders/layout helpers depend on this seam instead of reaching into renderers. | | `com.demcha.compose.engine.render` | Backend-neutral render contracts, handler registry, and render-pass session lifetime. | Add backend-neutral contracts here, backend-specific drawing elsewhere. | | `com.demcha.compose.engine.render.pdf` | Shared PDFBox primitives for the canonical fixed-layout backend: `PdfFont`, `GlyphFallbackLogger`, and the header/footer + watermark renderers under `helpers`. | Add canonical-shared PDF support here; per-fragment PDF drawing lives in the `PdfFragmentRenderHandler` implementations under `document.backend.fixed.pdf.handlers`. | diff --git a/docs/architecture/pagination-ordering.md b/docs/architecture/pagination-ordering.md index ad794dfb5..8ef135740 100644 --- a/docs/architecture/pagination-ordering.md +++ b/docs/architecture/pagination-ordering.md @@ -8,15 +8,15 @@ descendants must be paginated before their parent containers. During page breaking, a fixed leaf object may be moved to the next page instead of being split. When that happens, the leaf can increase the effective size of its parent container. -That parent size change is applied through the normal entity update flow: +That parent size change is applied through the normal compile flow: 1. the leaf detects that it does not fit on the current page 2. the leaf is shifted to the next page -3. the shift updates the parent container's `ContentSize` -4. the parent container must then compute its final `Placement` using the updated size +3. the shift updates the parent container's resolved content size +4. the parent container must then compute its final `FragmentPlacement` using the updated size If the parent container is processed before the child shift happens, the parent receives a stale -`Placement`. The `ContentSize` may later be correct, but the already-written `Placement.height` +placement. The content size may later be correct, but the already-written placement height will still be based on the old value. In practice, this looks like a container guide or span box that does not extend to cover the final child position. @@ -24,11 +24,12 @@ that does not extend to cover the final child position. This issue can be easy to misdiagnose because it does not belong to the leaf renderable itself. -Fixed leaf renderables such as `TextComponent` are all the same shape here: +Atomic leaf nodes — an image, a shape, a barcode, a single-line paragraph — are all the +same shape here: - they render a single box -- they are not `Breakable` -- when they do not fit, they are moved as a whole +- their `NodeDefinition` declares an atomic pagination policy, so `split` is never called +- when they do not fit the remaining page height, they relocate whole So if one leaf container fails to expand while another appears correct, the first thing to check is not the render implementation. The first thing to check is pagination order. @@ -50,36 +51,56 @@ For pagination, GraphCompose should follow this ordering contract: - if visual positions are equal, deeper entities should still win over shallower ones This rule guarantees that parent containers see all child-induced page shifts before their own -`Placement` is finalized. +`FragmentPlacement` is finalized. ## Current implementation shape -The current engine implementation does not enforce this rule with a pairwise ancestor comparator. -Instead it builds one deterministic traversal context for the whole pass through `LayoutTraversalContext`: - -- `ParentComponent` provides the authoritative parent relation -- `Entity.children` provides canonical sibling order -- the page breaker runs a priority-based topological walk where only nodes with no unresolved children can enter the ready queue -- within that ready queue, unrelated nodes are ordered by `ComputedPosition.y`, then depth, then UUID - -This keeps the algorithm fast on larger trees because it does not repeatedly walk parent chains during sorting. -It also keeps the rule renderer-agnostic: pagination still reasons over engine layout data, not PDF-specific output state. +The rule is satisfied structurally rather than by a comparator. `LayoutCompiler.compile` +walks the semantic roots of the `DocumentGraph` in document order, threading a single +`CompilerState` that owns the page cursor: + +- each node is prepared through its `NodeDefinition` into a `PreparedNode`, which + carries its measured size +- a composite recurses into its children *before* its own placement is finalized, so a + parent's box already reflects every child-induced page shift — the descendants-first + requirement above is a property of the recursion, not of a sort +- a splittable node that does not fit the remaining page height is divided through + `NodeDefinition.split`; an atomic one relocates whole +- placement produces `PlacedNode` and `PlacedFragment` records appended to one ordered + list per pass + +Because there is a single sequential emission pass, **emission order is render order** — +no backend re-sorts fragments to reconstruct z-order. Explicit stacking is resolved +locally where it is declared: `LayerStackGeometry` orders a layer stack's children by +`zIndex` with a stable sort, so equal values keep source order. + +Both properties matter beyond PDF: the PPTX backend consumes the same fragment stream in +the same order, which is why shape stacking matches between the two outputs without any +backend-specific ordering rule. ## Practical symptom If you see one of these behaviors, check pagination ordering first: - a container guide box stops too early after a child is moved to the next page -- `ContentSize` looks updated in logs, but `Placement.height` still has the old value +- the resolved content size looks updated in logs, but the emitted fragment height still has the old value - the same structural bug appears only for some leaf types or only in some align/margin setups ## Recommended debugging approach When debugging a pagination tree: -1. log the page-breaker processing order -2. compare parent and child `ComputedPosition.y` -3. check whether a child shift updates parent `ContentSize` -4. verify that the parent `Placement` is created after that update, not before it - -If step 4 is false, the bug is usually ordering, not rendering. +1. turn on the `com.demcha.compose.engine.pagination` logger at `DEBUG` — + `LayoutCompiler` emits `pagination.compile.start` (root count, inner height) and + `pagination.compile.end` (pages, nodes, fragments, duration), which bracket the pass + and tell you whether the page count itself is wrong +2. take a `DocumentSession.layoutSnapshot()` — it resolves the layout without rendering a + byte, so you can inspect placement in a plain unit test instead of reading a PDF +3. compare a parent's resolved box against its children's: a parent that does not + include a child's page-shift means the child was placed after the parent was finalized +4. check whether the node in question is atomic or splittable — a block that "jumps a + page too early" is usually an atomic leaf that cannot divide, not an ordering bug + +If step 3 shows the parent finalized first, the bug is ordering. If step 4 shows an +atomic leaf taller than the usable page height, it is a sizing problem and surfaces as +`AtomicNodeTooLargeException` rather than a silent misplacement. diff --git a/docs/contributing/release-process.md b/docs/contributing/release-process.md index c9293736a..cd2ca5be0 100644 --- a/docs/contributing/release-process.md +++ b/docs/contributing/release-process.md @@ -1,6 +1,8 @@ # GraphCompose release process -This is the canonical release runbook for GraphCompose 1.x. +This is the canonical release runbook for GraphCompose 2.x, cut from `develop`. +For a 1.9.x critical-fix release the same flow applies from the `1.x` maintenance +branch, on the older single-jar layout where `-pl .` is the engine. - Maven Central — `io.github.demchaav:graph-compose:` (canonical, from v1.6.6) - JitPack — `com.github.DemchaAV:GraphCompose:v` (legacy; resolves for callers pinned to v1.6.5 and earlier, no longer the documented install channel) @@ -17,7 +19,7 @@ Run this every time, in order. Stop on the first red gate and fix it before cont The shell setup and exact PowerShell commands live in the `graphcompose-release-engineer` skill (loaded via `Skill` tool). On Git Bash use `./mvnw` instead of `.\mvnw.cmd`; the gates are identical. -> **Cutting the 2.0 line?** This checklist is written for the 1.9.x line on `develop` (the old single-jar layout, where `-pl .` is the engine). For a 2.0 cut from `2.0-dev`, substitute throughout: branch `develop` → `2.0-dev`; the full gate `clean verify -pl .` → the whole reactor **`./mvnw -B -ntp clean verify`** (the root pom is the aggregator now); engine-only commands `-pl .` → `-pl :graph-compose-core`; `aggregator/pom.xml` → the root `pom.xml`, with the engine version in `core/pom.xml`. The cut itself uses `cut-release.ps1 -Branch 2.0-dev` — see [§2.G](#2g-cutting-the-20-line-branch-flow). +> **Backporting to 1.9.x?** This checklist is written for the 2.x reactor on `develop`. On the `1.x` maintenance branch substitute throughout: the full gate `./mvnw -B -ntp clean verify` → `clean verify -pl .`; engine-only commands `-pl :graph-compose-core` → `-pl .`; the root `pom.xml` reactor → `aggregator/pom.xml`, with the engine version in the standalone `pom.xml`. The cut itself uses `cut-release.ps1 -Branch 1.x`. ### A. Branch + working tree @@ -28,10 +30,11 @@ The shell setup and exact PowerShell commands live in the `graphcompose-release- ### B. Build + test gates -- [ ] `./mvnw -B -ntp -q clean verify -pl .` exits 0. Every test must pass — no skips, no flake retries. Confirm `Tests run: , Failures: 0, Errors: 0, Skipped: 0` from `target/surefire-reports/*.txt`. -- [ ] Examples module compiles cleanly: `./mvnw -B -ntp -q -f examples/pom.xml clean compile` exits 0. Catches `double → float` lossy narrowing and similar bugs that don't surface in the root module. -- [ ] All examples regenerate: `./mvnw -B -ntp -q -f examples/pom.xml exec:java -Dexec.mainClass=com.demcha.examples.GenerateAllExamples` produces 26+ `Generated:` lines, exits 0, and emits no `Fixed column ... is smaller than required natural width` or `Spanned cell ... requires extra width` errors. (Requires `./mvnw install -DskipTests -pl .` once first so the local `~/.m2` resolves the current SNAPSHOT/beta version.) -- [ ] Architecture-guard suite explicitly green: `./mvnw -B -ntp test -pl . -Dtest='CanonicalSurfaceGuardTest,DocumentationCoverageTest,DocumentationExamplesTest,InternalAnnotationCoverageTest,PublicApiNoEngineLeakTest,SemanticLayerNoPdfBoxDependencyTest,VersionConsistencyGuardTest'` exits 0. These guard against legacy-API leakage in docs and engine internals leaking into the public surface, and — via `VersionConsistencyGuardTest` — against version drift between the library pom, the aggregator, the inherited examples/benchmarks modules, and the README install snippets. They fail loudly when README/CHANGELOG drift from the canonical authoring surface. +- [ ] `./mvnw -B -ntp clean verify` exits 0 over the whole reactor. Every test must pass — no skips, no flake retries. Confirm `Tests run: , Failures: 0, Errors: 0, Skipped: 0` per module from `*/target/surefire-reports/*.txt`. **Read Maven's own exit code** — never end the command in a pipe, or the shell reports the last stage's status and a `BUILD FAILURE` slips through as `0`. +- [ ] Examples module compiles cleanly: `./mvnw -B -ntp -q -f examples/pom.xml clean compile` exits 0. Catches `double → float` lossy narrowing and similar bugs that don't surface in the engine module. +- [ ] All examples regenerate: `./mvnw -B -ntp -q -f examples/pom.xml exec:java -Dexec.mainClass=com.demcha.examples.GenerateAllExamples` produces 26+ `Generated:` lines, exits 0, and emits no `Fixed column ... is smaller than required natural width` or `Spanned cell ... requires extra width` errors. (Requires `./mvnw install -DskipTests` once first so the local `~/.m2` resolves the current SNAPSHOT version — any standalone goal that resolves train modules from `~/.m2`, including the `qa` suite and `javadoc:javadoc`, needs this after a version bump.) +- [ ] Architecture-guard suite explicitly green: `./mvnw -B -ntp test -pl :graph-compose-core -Dtest='CanonicalSurfaceGuardTest,DocumentationCoverageTest,InternalAnnotationCoverageTest,PublicApiNoEngineLeakTest,PackageMapGuardTest,VersionConsistencyGuardTest'` exits 0. These guard against legacy-API leakage in docs and engine internals leaking into the public surface, and — via `VersionConsistencyGuardTest` — against version drift between the train poms and the README install snippets. Note `PackageMapGuardTest` also scans the gitignored `docs/private/`, so a stale local note there fails a local run that CI cannot reproduce. +- [ ] Javadoc gate green on the published modules: `./mvnw -B -ntp javadoc:javadoc -pl :graph-compose-core,:graph-compose-render-pdf,:graph-compose-render-docx,:graph-compose-render-pptx,:graph-compose-templates,:graph-compose-testing` exits 0. The release profile publishes a javadoc jar per module with `failOnError=false`, so a broken link only surfaces here. ### C. Documentation freeze (matches target version) @@ -48,7 +51,10 @@ The shell setup and exact PowerShell commands live in the `graphcompose-release- The script's Step 1–4 mutates these. The agent only confirms the *current state is one the script can transition from*: -- [ ] The engine version lives in **five** sites that must stay in lockstep: the standalone library `pom.xml`, the reactor `aggregator/pom.xml`, the inherited `` version of `examples/pom.xml` and `benchmarks/pom.xml` (the children no longer pin their own `` — they inherit from `graph-compose-build`, and declare `${project.version}` rather than a literal), and the standalone `bundle/pom.xml` (`graph-compose-bundle`, whose `graph-compose` dep is `${project.version}`). All five read the same value: either the in-flight develop value or already the target. `VersionConsistencyGuardTest` asserts they agree; `cut-release.ps1` Step 1 moves all five (plus the README) together. Note `mvn -f aggregator/pom.xml versions:set` only rewrites the aggregator + its inheriting children — it does **not** touch the standalone `pom.xml` or `bundle/pom.xml`, so prefer the script. **`fonts/pom.xml` (`graph-compose-fonts`) is intentionally NOT in this set** — it carries an independent version line (see §2.D) and must be free to diverge from the engine. +- [ ] The train version lives in **13** poms that must stay in lockstep. `cut-release.ps1` Step 1 moves them together, so this list is the script's own array — keep the two in sync rather than re-deriving it by hand: + `pom.xml` (root reactor), `core/pom.xml`, `render-pdf/pom.xml`, `render-docx/pom.xml`, `render-pptx/pom.xml`, `templates/pom.xml`, `testing/pom.xml`, `wrapper/pom.xml`, `bundle/pom.xml`, plus the four that inherit ``: `examples/pom.xml`, `benchmarks/pom.xml`, `qa/pom.xml`, `coverage/pom.xml`. + All read the same value: either the in-flight develop value or already the target. `VersionConsistencyGuardTest` asserts they agree. Prefer the script over `versions:set`, which only rewrites a reactor and its inheriting children and misses the standalone poms. + **`fonts/pom.xml` and `emoji/pom.xml` are intentionally NOT in this set** — they carry independent version lines (see §2.D) and must be free to diverge from the engine. - [ ] `examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java` `GH_BASE` points to `/blob/develop`. The script flips it to `/blob/v` and regenerates `web/examples.json`. ### E. Tag must not exist @@ -62,13 +68,13 @@ The script's Step 1–4 mutates these. The agent only confirms the *current stat Running `pwsh ./scripts/cut-release.ps1 -Version ` performs: 1. **Pre-flight** — re-checks all of A above (branch, clean tree, in-sync, no existing tag). -2. **Bump versions** to `` across the library `pom.xml`, the `aggregator/pom.xml`, the inherited `` refs in `examples/pom.xml` and `benchmarks/pom.xml`, the standalone `bundle/pom.xml` (`graph-compose-bundle`), **and** the README Maven + Gradle install snippets — all in one pass, so `VersionConsistencyGuardTest` stays green at Step 5. (`fonts/pom.xml` is left alone — it versions independently; see §2.D.) +2. **Bump versions** to `` across all 13 train poms (§0.D), **and** the install snippets — the root `README.md` plus every per-module README, which a separate pass (`Update-ModuleReadmeInstallVersion`) rewrites because their coordinates carry an artifact suffix the root regex does not match. All in one pass, so `VersionConsistencyGuardTest` stays green at Step 5. (`fonts/pom.xml` and `emoji/pom.xml` are left alone — they version independently; see §2.D.) 3. **Date the CHANGELOG** — flips `## v — Planned` to `## v`. 3b. **Validate release metadata** — a fast, build-free pre-tag gate: the CHANGELOG is dated for the target, the README `Latest stable` prose block names the target, the README install snippet reads the target, and every published-train pom carries the target version. Fails immediately (before showcase / verify / commit / tag) if any is stale. This is the **only automated guard against the stale-tag-README bug** — `cut-release.ps1` does not rewrite the `Latest stable` prose block, and no test covers it. (The full per-module + Gradle + showcase snippet consistency is separately enforced by `VersionConsistencyGuardTest` in the Step-5 verify.) 4. **Switch ShowcaseMetadata GH_BASE** from `/blob/develop` to `/blob/v` and regenerate `web/examples.json`. -5. **`mvnw verify`** — full reactor sanity build (the older 1.x layout scopes to `-pl .`; the script auto-detects by `core/pom.xml`). Skip with `-SkipVerify` only if you just ran it. +5. **`mvnw verify`** — full reactor sanity build (the script auto-detects the layout by the presence of `core/pom.xml`, scoping to `-pl .` on the 1.x line). Skip with `-SkipVerify` only if you just ran it. 5b. **Binary-compatibility gate** — `mvnw -P japicmp verify -pl :graph-compose-core` against the published baseline (2.0 module layout only). Fails the cut if the tagged code breaks binary compatibility of the `graph-compose-core` public API (the japicmp profile lives only in `core/pom.xml`) with the baseline — a second line of defence independent of the PR-time CI japicmp job, which a direct-to-branch push could bypass. Skipped by `-SkipVerify`. -6. **Commit** as `Release v`. Files committed: the library `pom.xml`, `aggregator/pom.xml`, `examples/pom.xml`, `benchmarks/pom.xml`, `bundle/pom.xml`, `README.md` (install snippets), `CHANGELOG.md`, `ShowcaseMetadata.java`, `web/examples.json`, `web/index.html`, and `web/showcase/`. `examples/README.md` and any other docs are NOT touched by the script — fix those pre-release. +6. **Commit** as `Release v`. Staging is an explicit allow-list, not `git add -A`. It covers the 13 train poms, `README.md` and the seven per-module READMEs (`core`, `render-pdf`, `render-docx`, `render-pptx`, `templates`, `testing`, `wrapper`, `bundle`), `CHANGELOG.md`, `ShowcaseMetadata.java`, `web/examples.json`, `web/index.html`, `web/showcase/`, and the regenerated `assets/readme/repository_showcase_render.png`. `examples/README.md` and every other doc are NOT touched by the script — fix those pre-release. 7. **Annotated tag** `v` (`git tag -a -m "Release v"`). 8. **Push** `develop` and the tag to `origin` (skip with `-SkipPush`). @@ -101,7 +107,7 @@ Run within 1 hour of the tag push. Independent steps can run in parallel. 6. **Re-run all examples against the published artifact** — `./mvnw -f examples/pom.xml clean package` followed by `exec:java -Dexec.mainClass=com.demcha.examples.GenerateAllExamples`. Expect 26+ `Generated:` lines. 6b. **Run the external release-smoke suite** — once Central has indexed the train, dispatch the **Release Smoke** workflow ([`.github/workflows/release-smoke.yml`](../../.github/workflows/release-smoke.yml)) with `version=`, or run `bash scripts/release-smoke/run.sh --version `. This resolves every published coordinate from Maven Central in a clean, GraphCompose-evicted repository (no reactor / local install) and exercises the documented consumer scenarios — the wrapper renders PDF, `graph-compose-core` alone throws `MissingBackendException`, core+render-pdf renders, and templates/testing/bundle perform their roles. It is the authoritative "a real user can install and use this" check; the minimal step-5 snippet resolve is a faster subset. (Release smoke tests **published** artifacts, so it necessarily runs post-publish, not pre-tag.) 7. **Open the next development line** — `pwsh ./scripts/cut-release.ps1 -PostReleaseOnly`. This bumps the train poms to the next patch `-SNAPSHOT` (so develop builds are distinguishable from the release and the japicmp gate compares against it) **and** restores linkable "View Code" buttons by flipping ShowcaseMetadata back to `/blob/develop`. The README/showcase install snippets stay on the just-published release. -8. **GitHub Release — automated.** Pushing the `v` tag triggers [`.github/workflows/release.yml`](../../.github/workflows/release.yml): it re-runs `./mvnw clean verify -pl .` against the tagged commit, then creates the Release with that version's CHANGELOG section as the body (hyphenated tags like `v1.7.0-rc.1` ship as pre-releases; the step is idempotent — it edits the notes if the Release already exists). The workflow titles it `GraphCompose v`; for a **minor** release, edit the title to add the codename (`v1.4`=cinematic, `v1.5`=intuitive, `v1.6`=expressive; patches drop it). Create the Release by hand (`gh release create v --notes-file `) only if the workflow is unavailable. +8. **GitHub Release — automated.** Pushing the `v` tag triggers [`.github/workflows/release.yml`](../../.github/workflows/release.yml): it re-runs `./mvnw clean verify` over the whole reactor against the tagged commit, then creates the Release with that version's CHANGELOG section as the body (hyphenated tags like `v1.7.0-rc.1` ship as pre-releases; the step is idempotent — it edits the notes if the Release already exists). The workflow titles it `GraphCompose v`; for a **minor** release, edit the title to add the codename (`v1.4`=cinematic, `v1.5`=intuitive, `v1.6`=expressive; patches drop it). Create the Release by hand (`gh release create v --notes-file `) only if the workflow is unavailable. 9. **Maven Central publish — automated (from v1.6.6).** The same `v` tag push triggers [`.github/workflows/publish.yml`](../../.github/workflows/publish.yml): it re-runs `mvnw verify` at the tagged commit, signs the four artefacts (main / sources / javadoc / pom) with the repo's GPG key, and uploads to Maven Central via the `central-publishing-maven-plugin`. Hyphenated tags (`-rc`, `-alpha`, `-beta`, `-snapshot`) are skipped — those go only to the GitHub Release pre-release surface. `autoPublish=false` in the plugin config means the artefact lands in the Central validation queue; the maintainer flips the switch on [central.sonatype.com](https://central.sonatype.com) for the first publish, then can opt into auto-release in a follow-up. Verify via `mvn dependency:get -DgroupId=io.github.demchaav -DartifactId=graph-compose -Dversion=` once the artifact appears (usually 5–15 minutes after the workflow turns green). 10. **Optional**: GitHub Discussions announcement (mirror the prior release's style; close with *"author intent, not coordinates"*), LinkedIn post, r/java post. @@ -128,14 +134,14 @@ is a convenience aggregate `io.github.demchaav:graph-compose-bundle` (under ``, push a `fonts-vX.Y.Z` tag. That tag triggers [`publish-fonts.yml`](../../.github/workflows/publish-fonts.yml), which deploys only `graph-compose-fonts` to Central. Then bump - `` in `aggregator/pom.xml` (inherited by + `` in the root reactor `pom.xml` (inherited by examples + benchmarks) and `bundle/pom.xml` to the new fonts version so those - consumers pin it. The engine `pom.xml` does **not** carry this property — the + consumers pin it. `core/pom.xml` does **not** carry this property — the engine has no dependency on the fonts artifact (its tests read the fonts from the sibling module's source via ``). - **No fonts bootstrap for the engine.** Because the engine does not depend on - the fonts artifact, `./mvnw clean verify -pl .` builds standalone without the - fonts jar being published or installed. Only the consumer modules (examples, + the fonts artifact, `./mvnw clean verify -pl :graph-compose-core` builds it + without the fonts jar being published or installed. Only the consumer modules (examples, benchmarks) need `graph-compose-fonts` in the local repo — their CI jobs run `./mvnw -f fonts/pom.xml install` before building. @@ -155,9 +161,9 @@ fonts, so cutting an emoji release needs the same consumer re-pin. `emoji/pom.xml` ``, push an `emoji-vX.Y.Z` tag. That tag triggers [`publish-emoji.yml`](../../.github/workflows/publish-emoji.yml), which deploys only `graph-compose-emoji` to Central. Then bump `` - in `aggregator/pom.xml` (inherited by examples) and `bundle/pom.xml` to the new - emoji version so those consumers pin it — the `bundledEmojiVersionAgreesAcrossModules` - version guard enforces that the two stay in agreement. The engine `pom.xml` does + in the root reactor `pom.xml` (inherited by examples) and `bundle/pom.xml` to the + new emoji version so those consumers pin it — the `bundledEmojiVersionAgreesAcrossModules` + version guard enforces that the two stay in agreement. `core/pom.xml` does **not** carry this property — the engine has no dependency on the emoji artifact (its tests read the glyphs from the sibling module's source via ``). - **Regenerating the set.** `emoji/tools/build-emoji-set.py` rebuilds @@ -194,28 +200,30 @@ aggregate. They all carry the **same** version. skips Central for hyphenated tags (§2.B step 9). A beta is therefore installable from the GitHub pre-release (and from JitPack, which builds any tag), not from Central. -### 2.G Cutting the 2.0 line (branch flow) +### 2.G Branch flow -The 2.0 train is cut from **`2.0-dev`**, not `develop`; `cut-release.ps1` takes the -branch as a parameter. On the 2.0 layout the engine lives in `core/pom.xml` and the -repository root `pom.xml` is the reactor aggregator — the script bumps both (plus the -rest of the train) and its `Test-Path` guard skips `core/pom.xml` on the old 1.x layout, -so the same script serves both lines. +The 2.x train is cut from **`develop`**, which is `cut-release.ps1`'s default branch. +On this layout the engine lives in `core/pom.xml` and the repository root `pom.xml` is +the reactor — the script bumps both plus the rest of the train, and its `Test-Path` +guard skips `core/pom.xml` on the older 1.x layout, so the same script serves both lines. -- **Release candidate:** `pwsh ./scripts/cut-release.ps1 -Version 2.0.0-rc.1 -Branch 2.0-dev`. +- **Release candidate:** `pwsh ./scripts/cut-release.ps1 -Version -rc.1`. The hyphenated `-rc` tag ships to the GitHub pre-release surface only (Central skipped, §2.F). -- **GA:** `pwsh ./scripts/cut-release.ps1 -Version 2.0.0 -Branch 2.0-dev`. The plain - `v2.0.0` tag fires `publish.yml`, which deploys the eight-module train to Maven Central in - dependency order — that sequence is version-agnostic and needs no per-release change. - -At **2.0 GA** the branches take their long-term roles, in this order: - -1. **Cut `1.x` from the current `main` tip first** (the final 1.9.x commit), *before* moving - main: `git branch 1.x main && git push origin 1.x`. It receives 1.9.x critical fixes only. -2. **Fast-forward `main` to 2.0:** `git push origin 2.0-dev:main`. This is a clean - fast-forward because the pre-GA sync keeps `main`'s history an ancestor of `2.0-dev`. -3. **Repoint ongoing work:** `develop` becomes the 2.x working branch; `2.0-dev` retires. - From then on `cut-release.ps1` runs with its default `-Branch develop` again. +- **GA:** `pwsh ./scripts/cut-release.ps1 -Version `. The plain `v` tag fires + `publish.yml`, which deploys the eight-module train to Maven Central in dependency order + — that sequence is version-agnostic and needs no per-release change. +- **1.9.x backport:** `pwsh ./scripts/cut-release.ps1 -Version 1.9. -Branch 1.x`. + +After a GA the branches settle back into their standing roles: `main` is fast-forwarded +to the release, `develop` carries on as the 2.x working branch, and `1.x` continues to +take critical fixes only. `cut-release.ps1 -PostReleaseOnly` opens the next +`-SNAPSHOT` line and returns `ShowcaseMetadata.GH_BASE` to `/blob/develop`. + +> **Post-GA `-SNAPSHOT` naming.** `-PostReleaseOnly` always opens the next *patch* +> `-SNAPSHOT`. If the next release is a minor, bump the train to `-SNAPSHOT` +> by hand so the in-flight version matches the `@since` tags and the open CHANGELOG +> heading — nothing publishes from a SNAPSHOT, but the mismatch misleads every reader +> of a pom. ---