From 98c64821c122470fcc3f4d19b82c5517e4c8660d Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sat, 18 Jul 2026 08:34:07 +0100 Subject: [PATCH 1/2] test(qa): PPTX boundary guards, flagship deck example, CI install step --- .github/workflows/ci.yml | 10 ++- CHANGELOG.md | 19 +++++ README.md | 5 +- examples/README.md | 2 +- examples/pom.xml | 8 +++ .../demcha/examples/GenerateAllExamples.java | 2 + .../flagships/EngineDeckPptxExample.java | 52 ++++++++++++++ qa/pom.xml | 9 +++ .../CrossModuleBoundaryArchTest.java | 69 +++++++++++++++---- .../ShapeOutlineRenderCoverageTest.java | 27 ++++++-- .../pptx/handlers/PptxInlineGeometry.java | 7 ++ 11 files changed, 190 insertions(+), 20 deletions(-) create mode 100644 examples/src/main/java/com/demcha/examples/flagships/EngineDeckPptxExample.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 134fee62b..bd429d3bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -148,7 +148,8 @@ jobs: cache: maven - name: Build and run tests - # Reactor slice: the engine, the render-docx / render-pptx semantic backends, + # Reactor slice: the engine, the render-docx semantic backend, the + # render-pptx fixed-layout backend, # the graph-compose wrapper + graph-compose-bundle (the published empty-jar # aggregates — covered here so a packaging break is caught on every push, not # only via the examples job), the extracted graph-compose-testing module, the @@ -236,6 +237,13 @@ jobs: # dependency) so the examples build resolves it. run: ./mvnw -B -ntp -f render-docx/pom.xml -DskipTests install + - name: Install graph-compose-render-pptx (consumed by the examples module) + # The Engine Deck PPTX example renders the flagship deck through the + # fixed-layout PPTX backend, which is not on Central. Install it after + # the wrapper (its core + render-pdf dependencies) so the examples + # build resolves it. + run: ./mvnw -B -ntp -f render-pptx/pom.xml -DskipTests install + - name: Install graph-compose-testing (consumed by the examples module) # LayoutSnapshotRegressionExample uses LayoutSnapshotJson and EngineDeckData # reads benchmark JSON through the jackson it brings; the testing module is diff --git a/CHANGELOG.md b/CHANGELOG.md index a8e9c6c4f..a69833b40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -129,6 +129,18 @@ follow semantic versioning; release dates are ISO 8601. - `docs/architecture/backend-capability-matrix.md` — a per-capability matrix of what each render backend supports and which class implements it, maintained as part of every capability-changing PR. +- The README's backend descriptions now present `graph-compose-render-pptx` + as the fixed-layout, geometry-identical PowerPoint backend (one page per + editable slide) instead of a semantic exporter, with its own + quick-start dependency row; the Engine Deck flagship example additionally + renders as a .pptx deck (`EngineDeckPptxExample`, part of + `GenerateAllExamples`). + +### Build + +- The examples CI job installs `graph-compose-render-pptx` before generating, + so the Engine Deck PPTX example resolves the backend the same way the DOCX + example resolves its own. ### Tests @@ -158,6 +170,13 @@ follow semantic versioning; release dates are ISO 8601. default-output-file and empty-session failure modes; the backend-free missing-format diagnostics were already pinned by the core's `MissingBackendContractTest`. +- The qa module now guards the PPTX backend's boundaries at bytecode level: + POI stays a PPTX-backend implementation detail (the engine, the PDF backend, + and the templates must not touch it), the render backends stay + template-agnostic, and the PDF backend must not depend on the PPTX artifact + (the reverse dependency is by design). The `ShapeOutline` exhaustiveness + guard now renders every permit through both fixed-layout backends, so a new + outline kind cannot silently miss a PPTX dispatch branch. ## v2.0.0 — 2026-07-13 diff --git a/README.md b/README.md index 8b1eef53e..62bb16dcf 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Sits between **iText** (low-level page primitives) and **JasperReports** (XML-te The **module-first** release — the single jar becomes a family of per-concern artifacts, so you install exactly what you render. - **Lean engine** — `graph-compose-core` is the document model, DSL, themes, and deterministic layout with **no PDFBox, POI, or template code** on its dependency tree. Backends plug in through a `ServiceLoader` seam; a core-only classpath asked to render throws `MissingBackendException` naming the artifact to add. -- **Opt-in render backends** — `graph-compose-render-pdf` (PDFBox 3.0, full DSL coverage), `graph-compose-render-docx` and `graph-compose-render-pptx` (Apache POI, semantic export). +- **Opt-in render backends** — `graph-compose-render-pdf` (PDFBox 3.0, full DSL coverage), `graph-compose-render-pptx` (Apache POI, geometry-identical PowerPoint decks from the same resolved layout — one page per editable slide; clipped regions land as pixel-exact pictures), `graph-compose-render-docx` (Apache POI, semantic export). - **`graph-compose` stays a drop-in** — the 1.x coordinate is now a thin wrapper over core + the PDF backend, so existing callers upgrade with **no code and no dependency change**. - **Templates are their own artifact** — the CV / cover-letter / invoice / proposal preset families moved to `graph-compose-templates` (imports unchanged). This is the [one dependency-level break](./docs/migration/v2.0.0-modules.md#the-one-break-templates) of the split. - **`graph-compose-bundle`** — one batteries-included coordinate: PDF stack + templates + fonts + colour emoji. @@ -91,7 +91,8 @@ dependencies { implementation("io.github.demchaav:graph-compose:2.0.0") } > | **Batteries-included** (PDF + templates + fonts + emoji) | `graph-compose-bundle` | > | **Lean core, bring your own backend** | `graph-compose-core` | > | **Built-in CV / cover-letter / invoice / proposal templates** | add `graph-compose-templates` | -> | **DOCX / PPTX export** | add `graph-compose-render-docx` / `graph-compose-render-pptx` | +> | **PowerPoint deck, geometry-identical to the PDF** | add `graph-compose-render-pptx` | +> | **DOCX export (semantic)** | add `graph-compose-render-docx` | > > Every 2.0 coordinate shares the `graph-compose` version (the fonts and emoji companions > keep their own lines). A bare `graph-compose-core` renders nothing until a backend is on diff --git a/examples/README.md b/examples/README.md index 1aadb5f4b..5451f171c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -61,7 +61,7 @@ are with the canonical DSL, then jump to its detailed section below. | [Cover Letter](#cover-letter) | One-page `BusinessTheme.modern()` cover letter with section presets | [PDF](../assets/readme/examples/cover-letter.pdf) · [Source](src/main/java/com/demcha/examples/templates/coverletter/CoverLetterFileExample.java) | | [Module-first Profile](#module-first-profile) | Authoring directly against `DocumentSession.module(...).paragraph(...)` — DSL-direct, no template | [PDF](../assets/readme/examples/module-first-profile.pdf) · [Source](src/main/java/com/demcha/examples/flagships/ModuleFirstFileExample.java) | | **Engine Showcase** | Single-page cinematic brand promo — semantic-graph → polished-PDFs visual metaphor with rounded clip frame, magazine headline lockup, KPI cards, capability columns; source of the README hero image | [Source](src/main/java/com/demcha/examples/flagships/EngineShowcase.java) | -| **Engine Deck** | Multi-page **landscape** capability deck — page 1 is a banner infographic (DSL code → engine → backends → **real rendered-document thumbnails**), then an authoring-pipeline walkthrough, and two pages of **real benchmark data** (GraphCompose vs iText 9 vs JasperReports) loaded from a bundled result file and drawn as tables + native charts; the landscape companion to Engine Showcase | [PDF](../assets/readme/examples/engine-deck.pdf) · [Source](src/main/java/com/demcha/examples/flagships/EngineDeckExample.java) | +| **Engine Deck** | Multi-page **landscape** capability deck — page 1 is a banner infographic (DSL code → engine → backends → **real rendered-document thumbnails**), then an authoring-pipeline walkthrough, and two pages of **real benchmark data** (GraphCompose vs iText 9 vs JasperReports) loaded from a bundled result file and drawn as tables + native charts; the landscape companion to Engine Showcase. The same composition also renders as a **geometry-identical PowerPoint deck** (one page = one editable slide) through `buildPptx()` | [PDF](../assets/readme/examples/engine-deck.pdf) · [Source](src/main/java/com/demcha/examples/flagships/EngineDeckExample.java) · [PPTX source](src/main/java/com/demcha/examples/flagships/EngineDeckPptxExample.java) | ### 🧱 Core DSL diff --git a/examples/pom.xml b/examples/pom.xml index 3dea9e92f..26c76243d 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -76,6 +76,14 @@ ${graphcompose.version} + + + io.github.demchaav + graph-compose-render-pptx + ${graphcompose.version} + + diff --git a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java index 67ae395c6..72e91314b 100644 --- a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java +++ b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java @@ -45,6 +45,7 @@ import com.demcha.examples.features.transforms.TransformsExample; import com.demcha.examples.flagships.BusinessReportExample; import com.demcha.examples.flagships.EngineDeckExample; +import com.demcha.examples.flagships.EngineDeckPptxExample; import com.demcha.examples.flagships.FeatureCatalogExample; import com.demcha.examples.flagships.FinancialReportExample; import com.demcha.examples.flagships.MasterShowcaseExample; @@ -212,6 +213,7 @@ public static void main(String[] args) throws Exception { System.out.println("Generated: " + FeatureCatalogExample.generate()); System.out.println("Generated: " + BusinessReportExample.generate()); System.out.println("Generated: " + EngineDeckExample.generate()); + System.out.println("Generated: " + EngineDeckPptxExample.generate()); System.out.println("Generated: " + FinancialReportExample.generate()); System.out.println("Generated: " + BookTemplateExample.generate()); System.out.println("Generated: " + PoetryTitlePageExample.generate()); diff --git a/examples/src/main/java/com/demcha/examples/flagships/EngineDeckPptxExample.java b/examples/src/main/java/com/demcha/examples/flagships/EngineDeckPptxExample.java new file mode 100644 index 000000000..6cdb64d66 --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/flagships/EngineDeckPptxExample.java @@ -0,0 +1,52 @@ +package com.demcha.examples.flagships; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentPageSize; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.examples.support.ExampleOutputPaths; + +import java.nio.file.Path; + +/** + * The Engine Deck flagship rendered as a PowerPoint deck: the exact + * composition of {@link EngineDeckExample} — banner infographic, authoring + * pipeline, and two benchmark pages — built once and emitted through the + * fixed-layout PPTX backend via {@link DocumentSession#buildPptx()}. One + * resolved page becomes one identically-sized slide with the same geometry + * the PDF carries, so the .pptx opens in PowerPoint as a slide-per-page copy + * of the PDF deck — editable shapes and text frames, except clipped regions, + * which land as pixel-exact pictures. + */ +public final class EngineDeckPptxExample { + + private EngineDeckPptxExample() { + } + + /** + * Builds the landscape showcase deck as a .pptx. + * + * @return the generated file path + * @throws Exception when rendering or icon IO fails + */ + public static Path generate() throws Exception { + Path outputFile = ExampleOutputPaths.prepare("flagships", "engine-deck.pptx"); + try (DocumentSession document = GraphCompose.document(outputFile) + .pageSize(DocumentPageSize.A4.landscape()) + .margin(16, 16, 30, 16) + .create()) { + EngineDeckExample.compose(document); + document.buildPptx(); + } + return outputFile; + } + + /** + * Generates the deck from the command line. + * + * @param args unused + * @throws Exception when rendering fails + */ + public static void main(String[] args) throws Exception { + System.out.println("Generated: " + generate()); + } +} diff --git a/qa/pom.xml b/qa/pom.xml index 48c54c247..88100b323 100644 --- a/qa/pom.xml +++ b/qa/pom.xml @@ -89,6 +89,15 @@ ${project.version} test + + + io.github.demchaav + graph-compose-render-pptx + ${project.version} + test + diff --git a/qa/src/test/java/com/demcha/compose/architecture/CrossModuleBoundaryArchTest.java b/qa/src/test/java/com/demcha/compose/architecture/CrossModuleBoundaryArchTest.java index 7a22c0a82..35d4239a4 100644 --- a/qa/src/test/java/com/demcha/compose/architecture/CrossModuleBoundaryArchTest.java +++ b/qa/src/test/java/com/demcha/compose/architecture/CrossModuleBoundaryArchTest.java @@ -12,9 +12,10 @@ /** * Cross-module architecture guards, enforced at bytecode level. The engine * ({@code graph-compose-core}), the PDF backend ({@code graph-compose-render-pdf}), - * and the built-in templates ({@code graph-compose-templates}) only meet on one - * classpath here in the qa module, so the boundaries between them can - * only be asserted from here. + * the PPTX backend ({@code graph-compose-render-pptx}), and the built-in + * templates ({@code graph-compose-templates}) only meet on one classpath here + * in the qa module, so the boundaries between them can only be + * asserted from here. * *

These complement the intra-core {@code ModuleBoundaryArchTest} in the engine * module. In particular the templates → engine boundary went unguarded when @@ -24,8 +25,8 @@ class CrossModuleBoundaryArchTest { // Every module's production classes on the qa classpath (engine + render-pdf + - // templates + testing). DO_NOT_INCLUDE_TESTS drops qa's own test classes, which - // legitimately depend on all of them. + // render-pptx + templates + testing). DO_NOT_INCLUDE_TESTS drops qa's own test + // classes, which legitimately depend on all of them. private static final JavaClasses MODULE_CLASSES = new ClassFileImporter() .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) .importPackages("com.demcha.compose"); @@ -35,6 +36,10 @@ class CrossModuleBoundaryArchTest { "com.demcha.compose.document.backend.fixed.pdf..", "com.demcha.compose.engine.render.pdf.." }; + private static final String[] PPTX_BACKEND = { + "com.demcha.compose.document.backend.fixed.pptx..", + "com.demcha.compose.document.backend.semantic.pptx.." + }; @Test void importedTheTemplatesAndEngineModuleClasses() { @@ -45,6 +50,9 @@ void importedTheTemplatesAndEngineModuleClasses() { .anyMatch(c -> c.getPackageName().startsWith("com.demcha.compose.document.templates"))) .as("the templates classes must be on the qa classpath so the boundary rules have subjects") .isTrue(); + assertThat(MODULE_CLASSES.contain("com.demcha.compose.document.backend.fixed.pptx.PptxFixedLayoutBackend")) + .as("the PPTX backend classes must be on the qa classpath so the POI rules have subjects") + .isTrue(); } @Test @@ -63,21 +71,58 @@ void templatesDoNotDependOnTheEngine() { void templatesStayBackendNeutral() { ArchRule rule = noClasses() .that().resideInAPackage(TEMPLATES) - .should().dependOnClassesThat().resideInAPackage("org.apache.pdfbox..") - .because("templates are pure authoring code over the canonical DSL; PDFBox lives " - + "behind the graph-compose-render-pdf backend seam, not in a preset"); + .should().dependOnClassesThat() + .resideInAnyPackage("org.apache.pdfbox..", "org.apache.poi..") + .because("templates are pure authoring code over the canonical DSL; PDFBox and POI " + + "live behind the render-backend seams, not in a preset"); rule.check(MODULE_CLASSES); } @Test - void thePdfBackendDoesNotDependOnTemplates() { + void theRenderBackendsDoNotDependOnTemplates() { ArchRule rule = noClasses() - .that().resideInAnyPackage(PDF_BACKEND) + .that().resideInAnyPackage(concat(PDF_BACKEND, PPTX_BACKEND)) .should().dependOnClassesThat().resideInAPackage(TEMPLATES) - .because("the render backend is template-agnostic — it renders the resolved layout " - + "graph and knows nothing about the built-in presets"); + .because("the render backends are template-agnostic — they render the resolved " + + "layout graph and know nothing about the built-in presets"); + + rule.check(MODULE_CLASSES); + } + + @Test + void onlyRenderBackendsDependOnPoi() { + // The DOCX semantic backend also legitimately uses POI; it is not on + // the qa classpath today, but the exclusion keeps this rule correct + // the day render-docx joins for its own boundary guards. + ArchRule rule = noClasses() + .that().resideOutsideOfPackages(concat(PPTX_BACKEND, + new String[]{"com.demcha.compose.document.backend.semantic.docx.."})) + .should().dependOnClassesThat().resideInAPackage("org.apache.poi..") + .because("POI is a render-backend implementation detail — the engine, the PDF " + + "backend, and the templates must stay free of it so the core artifact " + + "never drags the POI dependency in"); rule.check(MODULE_CLASSES); } + + @Test + void thePdfBackendDoesNotDependOnThePptxBackend() { + // The reverse dependency is by design: the PPTX backend reuses the PDF + // measurement stack and raster sub-renders for geometry identity. + ArchRule rule = noClasses() + .that().resideInAnyPackage(PDF_BACKEND) + .should().dependOnClassesThat().resideInAnyPackage(PPTX_BACKEND) + .because("the PDF backend is the reference implementation and must remain " + + "deployable without the PPTX artifact"); + + rule.check(MODULE_CLASSES); + } + + private static String[] concat(String[] first, String[] second) { + String[] merged = new String[first.length + second.length]; + System.arraycopy(first, 0, merged, 0, first.length); + System.arraycopy(second, 0, merged, first.length, second.length); + return merged; + } } diff --git a/qa/src/test/java/com/demcha/compose/document/architecture/ShapeOutlineRenderCoverageTest.java b/qa/src/test/java/com/demcha/compose/document/architecture/ShapeOutlineRenderCoverageTest.java index cfe01d81f..e3bd7f37f 100644 --- a/qa/src/test/java/com/demcha/compose/document/architecture/ShapeOutlineRenderCoverageTest.java +++ b/qa/src/test/java/com/demcha/compose/document/architecture/ShapeOutlineRenderCoverageTest.java @@ -24,12 +24,19 @@ * Exhaustiveness guard: every {@link ShapeOutline} permit must render through * all three outline-consuming surfaces — the shape-container outline * fill/stroke, the shape-container clip path, and the inline-shape run — - * without throwing. Each surface dispatches on the outline kind with an - * {@code instanceof} chain that ends in an {@code IllegalStateException}; this - * test reflects over {@code getPermittedSubclasses()} so the next permit added - * to the sealed type cannot silently miss a render branch (the lesson from the + * without throwing, on both fixed-layout backends. Each PDF surface + * dispatches on the outline kind with an {@code instanceof} chain that ends in + * an {@code IllegalStateException}; this test reflects over + * {@code getPermittedSubclasses()} so the next permit added to the sealed type + * cannot silently miss a render branch (the lesson from the * {@code ShapeOutline.Path} clipper, where the inline-shape switch was missed). * + *

On PPTX the discriminating chains are the fill-geometry resolution in + * core (shared with PDF) and {@code PptxInlineGeometry.drawOutline}'s own + * dispatch with its terminal throw; the clip surface renders through the PDF + * raster fallback, so its PPTX assertion re-proves the PDF chain rather than a + * PPTX-native one.

+ * *

The fill surface is isolated with {@link ClipPolicy#OVERFLOW_VISIBLE}: the * clip test exercises {@code ShapeContainerDefinition}'s fill-geometry chain * transitively (the outline fragment is emitted whether or not the container @@ -82,6 +89,10 @@ void everyOutlineFillsAContainerWithoutThrowing() throws Exception { assertThat(new String(pdf, 0, 5, StandardCharsets.US_ASCII)) .as("fill render of " + outline.getClass().getSimpleName()) .isEqualTo("%PDF-"); + byte[] pptx = session.toPptxBytes(); + assertThat(new String(pptx, 0, 2, StandardCharsets.US_ASCII)) + .as("PPTX fill render of " + outline.getClass().getSimpleName()) + .isEqualTo("PK"); } } } @@ -104,6 +115,10 @@ void everyOutlineClipsInAContainerWithoutThrowing() throws Exception { assertThat(new String(pdf, 0, 5, StandardCharsets.US_ASCII)) .as("clip render of " + outline.getClass().getSimpleName()) .isEqualTo("%PDF-"); + byte[] pptx = session.toPptxBytes(); + assertThat(new String(pptx, 0, 2, StandardCharsets.US_ASCII)) + .as("PPTX clip render of " + outline.getClass().getSimpleName()) + .isEqualTo("PK"); } } } @@ -124,6 +139,10 @@ void everyOutlineRendersAsAnInlineShapeWithoutThrowing() throws Exception { assertThat(new String(pdf, 0, 5, StandardCharsets.US_ASCII)) .as("inline render of " + outline.getClass().getSimpleName()) .isEqualTo("%PDF-"); + byte[] pptx = session.toPptxBytes(); + assertThat(new String(pptx, 0, 2, StandardCharsets.US_ASCII)) + .as("PPTX inline render of " + outline.getClass().getSimpleName()) + .isEqualTo("PK"); } } } diff --git a/render-pptx/src/main/java/com/demcha/compose/document/backend/fixed/pptx/handlers/PptxInlineGeometry.java b/render-pptx/src/main/java/com/demcha/compose/document/backend/fixed/pptx/handlers/PptxInlineGeometry.java index 752997ba2..416499355 100644 --- a/render-pptx/src/main/java/com/demcha/compose/document/backend/fixed/pptx/handlers/PptxInlineGeometry.java +++ b/render-pptx/src/main/java/com/demcha/compose/document/backend/fixed/pptx/handlers/PptxInlineGeometry.java @@ -42,6 +42,13 @@ static void drawOutline(XSLFShapeContainer surface, drawPath(surface, polygonPath(polygon.points(), box), fill, stroke); } else if (outline instanceof ShapeOutline.Path path) { drawPath(surface, path(path.segments(), box), fill, stroke); + } else { + // Exhaustiveness backstop: silently rendering nothing would let a + // new ShapeOutline permit ship without a PPTX branch — the qa + // coverage guard relies on this throw to catch it. + throw new IllegalStateException( + "No PPTX inline-geometry branch for outline kind " + + outline.getClass().getName() + "."); } } From 657e7e28c820ecf268de588b9ba235ab1e130372 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sat, 18 Jul 2026 09:02:04 +0100 Subject: [PATCH 2/2] ci(examples): install render-pptx after its test-scope testing dependency --- .github/workflows/ci.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd429d3bf..0723214e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -237,13 +237,6 @@ jobs: # dependency) so the examples build resolves it. run: ./mvnw -B -ntp -f render-docx/pom.xml -DskipTests install - - name: Install graph-compose-render-pptx (consumed by the examples module) - # The Engine Deck PPTX example renders the flagship deck through the - # fixed-layout PPTX backend, which is not on Central. Install it after - # the wrapper (its core + render-pdf dependencies) so the examples - # build resolves it. - run: ./mvnw -B -ntp -f render-pptx/pom.xml -DskipTests install - - name: Install graph-compose-testing (consumed by the examples module) # LayoutSnapshotRegressionExample uses LayoutSnapshotJson and EngineDeckData # reads benchmark JSON through the jackson it brings; the testing module is @@ -251,6 +244,14 @@ jobs: # dependency) before the examples build resolves it. run: ./mvnw -B -ntp -f testing/pom.xml -DskipTests install + - name: Install graph-compose-render-pptx (consumed by the examples module) + # The Engine Deck PPTX example renders the flagship deck through the + # fixed-layout PPTX backend, which is not on Central. Install it after + # the wrapper (its core + render-pdf compile dependencies) AND the + # testing module — -DskipTests skips execution but Maven still + # resolves render-pptx's test-scope graph-compose-testing dependency. + run: ./mvnw -B -ntp -f render-pptx/pom.xml -DskipTests install + - name: Install graph-compose-templates (consumed by the examples module) # The flagship + template examples render the built-in CV/invoice/proposal # presets, which now ship in graph-compose-templates.