diff --git a/CHANGELOG.md b/CHANGELOG.md index c495bec63..6682f247d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -127,6 +127,21 @@ follow semantic versioning; release dates are ISO 8601. ### Documentation +- **The example catalogue is built one way, and it starts empty.** The runner only ever + wrote, so a renamed or deleted example left its old document behind — published to the + site, counted by the guards that read the tree, and indistinguishable from a current + one. Generation now clears the output tree first, inside the runner itself, so the + release script, CI and the tests all get the same guarantee instead of the one path + that happened to remember. `ShowcaseSync` removes the directories as well as the files + it replaces, which makes the published tree a function of the catalogue rather than a + record of everything ever generated. +- **Nine committed previews are gone and two examples now run.** Seven were pre-2.0 + renders of CV presets whose current versions the runner already produced, so the + gallery linked the older snapshot of each; those links follow the current render and + the stale files are removed. Two more, an invoice and a proposal, were left by a rename + and nothing linked them at all. The cover-letter example and a custom-theme CV were + never wired into the runner, so one of them backed a link with a file no code produced + — both are generated now, and both have a showcase entry. - **The template-authoring guide describes the packages that exist.** It told a contributor to put a new family under `templates..v2` in exactly five sub-packages, one of them a per-family `theme/` — a layout 2.0 replaced. A family has diff --git a/assets/readme/examples/cover-letter.pdf b/assets/readme/examples/cover-letter.pdf index 3cf941676..bd5ed48a8 100644 Binary files a/assets/readme/examples/cover-letter.pdf and b/assets/readme/examples/cover-letter.pdf differ diff --git a/assets/readme/examples/cv-classic-serif.pdf b/assets/readme/examples/cv-classic-serif-v2.pdf similarity index 87% rename from assets/readme/examples/cv-classic-serif.pdf rename to assets/readme/examples/cv-classic-serif-v2.pdf index c0ec3d9ff..67919255f 100644 Binary files a/assets/readme/examples/cv-classic-serif.pdf and b/assets/readme/examples/cv-classic-serif-v2.pdf differ diff --git a/assets/readme/examples/cv-compact-mono.pdf b/assets/readme/examples/cv-compact-mono-v2.pdf similarity index 82% rename from assets/readme/examples/cv-compact-mono.pdf rename to assets/readme/examples/cv-compact-mono-v2.pdf index 1ecfdc021..a862ba987 100644 Binary files a/assets/readme/examples/cv-compact-mono.pdf and b/assets/readme/examples/cv-compact-mono-v2.pdf differ diff --git a/assets/readme/examples/cv-engineering-resume.pdf b/assets/readme/examples/cv-engineering-resume-v2.pdf similarity index 80% rename from assets/readme/examples/cv-engineering-resume.pdf rename to assets/readme/examples/cv-engineering-resume-v2.pdf index 1b2312a6d..1c14c11e8 100644 Binary files a/assets/readme/examples/cv-engineering-resume.pdf and b/assets/readme/examples/cv-engineering-resume-v2.pdf differ diff --git a/assets/readme/examples/cv-modern-professional-v2.pdf b/assets/readme/examples/cv-modern-professional-v2.pdf new file mode 100644 index 000000000..0b9c4562f Binary files /dev/null and b/assets/readme/examples/cv-modern-professional-v2.pdf differ diff --git a/assets/readme/examples/cv-modern-professional.pdf b/assets/readme/examples/cv-modern-professional.pdf deleted file mode 100644 index f75d5989e..000000000 Binary files a/assets/readme/examples/cv-modern-professional.pdf and /dev/null differ diff --git a/assets/readme/examples/cv-nordic-clean.pdf b/assets/readme/examples/cv-nordic-clean-v2.pdf similarity index 83% rename from assets/readme/examples/cv-nordic-clean.pdf rename to assets/readme/examples/cv-nordic-clean-v2.pdf index c99d95033..80a6094d0 100644 Binary files a/assets/readme/examples/cv-nordic-clean.pdf and b/assets/readme/examples/cv-nordic-clean-v2.pdf differ diff --git a/assets/readme/examples/cv-panel.pdf b/assets/readme/examples/cv-panel-v2.pdf similarity index 80% rename from assets/readme/examples/cv-panel.pdf rename to assets/readme/examples/cv-panel-v2.pdf index 97dea47d0..ce10bbe5f 100644 Binary files a/assets/readme/examples/cv-panel.pdf and b/assets/readme/examples/cv-panel-v2.pdf differ diff --git a/assets/readme/examples/cv-timeline-minimal.pdf b/assets/readme/examples/cv-timeline-minimal-v2.pdf similarity index 94% rename from assets/readme/examples/cv-timeline-minimal.pdf rename to assets/readme/examples/cv-timeline-minimal-v2.pdf index 6443f0e50..cc2b9dc94 100644 Binary files a/assets/readme/examples/cv-timeline-minimal.pdf and b/assets/readme/examples/cv-timeline-minimal-v2.pdf differ diff --git a/assets/readme/examples/invoice.pdf b/assets/readme/examples/invoice.pdf deleted file mode 100644 index cb52dbbab..000000000 Binary files a/assets/readme/examples/invoice.pdf and /dev/null differ diff --git a/assets/readme/examples/proposal.pdf b/assets/readme/examples/proposal.pdf deleted file mode 100644 index 6081db2c9..000000000 Binary files a/assets/readme/examples/proposal.pdf and /dev/null differ diff --git a/examples/README.md b/examples/README.md index 128c01f12..94d3dde57 100644 --- a/examples/README.md +++ b/examples/README.md @@ -56,7 +56,7 @@ are with the canonical DSL, then jump to its detailed section below. | Example | What it shows | Preview · Source | |---|---|---| -| [CV — single template](#cv-single-template) | One CV via `ModernProfessional.create()` on a `CvDocument` | [PDF](../assets/readme/examples/cv-modern-professional.pdf) · [Source](src/main/java/com/demcha/examples/templates/cv/CvFileExample.java) | +| [CV — single template](#cv-single-template) | One CV via `ModernProfessional.create()` on a `CvDocument` | [PDF](../assets/readme/examples/cv-modern-professional-v2.pdf) · [Source](src/main/java/com/demcha/examples/templates/cv/v2/CvModernV2Example.java) | | [Invoice — cinematic V2](#invoice-cinematic-v2) | `ModernInvoice + BrandTheme.invoiceModern()` — the recommended invoice path | [PDF](../assets/readme/examples/invoice-cinematic.pdf) · [Source](src/main/java/com/demcha/examples/templates/invoice/InvoiceCinematicFileExample.java) | | [Cover Letter](#cover-letter) | One-page cover letter composed in the canonical DSL, section presets carrying the hierarchy | [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) | @@ -205,8 +205,8 @@ One CV rendered through the layered template surface: The preset is one final class with `create()` / `create(BrandTheme)` factories — copy-and-tweak rather than fork-a-monolith. -[📄 View PDF](../assets/readme/examples/cv-modern-professional.pdf) · -[📜 Full source](src/main/java/com/demcha/examples/templates/cv/CvFileExample.java) +[📄 View PDF](../assets/readme/examples/cv-modern-professional-v2.pdf) · +[📜 Full source](src/main/java/com/demcha/examples/templates/cv/v2/CvModernV2Example.java) ### CV — template gallery @@ -218,15 +218,19 @@ preset for your own CV product. Each preset is a one-liner factory | Variant | PDF | |---|---| -| Modern professional | [PDF](../assets/readme/examples/cv-modern-professional.pdf) | -| Nordic clean | [PDF](../assets/readme/examples/cv-nordic-clean.pdf) | -| Classic serif | [PDF](../assets/readme/examples/cv-classic-serif.pdf) | -| Compact mono | [PDF](../assets/readme/examples/cv-compact-mono.pdf) | -| Timeline minimal | [PDF](../assets/readme/examples/cv-timeline-minimal.pdf) | -| Engineering resume | [PDF](../assets/readme/examples/cv-engineering-resume.pdf) | -| Panel | [PDF](../assets/readme/examples/cv-panel.pdf) | +| Modern professional | [PDF](../assets/readme/examples/cv-modern-professional-v2.pdf) | +| Nordic clean | [PDF](../assets/readme/examples/cv-nordic-clean-v2.pdf) | +| Classic serif | [PDF](../assets/readme/examples/cv-classic-serif-v2.pdf) | +| Compact mono | [PDF](../assets/readme/examples/cv-compact-mono-v2.pdf) | +| Timeline minimal | [PDF](../assets/readme/examples/cv-timeline-minimal-v2.pdf) | +| Engineering resume | [PDF](../assets/readme/examples/cv-engineering-resume-v2.pdf) | +| Panel | [PDF](../assets/readme/examples/cv-panel-v2.pdf) | | Executive · BoxedSections · CenteredHeadline · BlueBanner · EditorialBlue · SidebarPortrait · MonogramSidebar · MintEditorial · MinimalUnderlined | run the gallery to render | +The previews above come from the per-preset examples under +[`templates/cv/v2/`](src/main/java/com/demcha/examples/templates/cv/v2), which the +catalogue runner generates. The gallery below renders every preset from one entry point +instead, and is not part of that run — use it to produce the whole set locally: [📜 Full source](src/main/java/com/demcha/examples/templates/cv/CvTemplateGalleryFileExample.java) ### Cover letter — template gallery diff --git a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java index b31065cb1..d2276cc3c 100644 --- a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java +++ b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java @@ -43,6 +43,7 @@ import com.demcha.examples.features.title.BookTemplateExample; import com.demcha.examples.features.title.PoetryTitlePageExample; import com.demcha.examples.features.transforms.TransformsExample; +import com.demcha.examples.support.ExampleOutputPaths; import com.demcha.examples.flagships.BusinessReportExample; import com.demcha.examples.flagships.BusinessReportPptxExample; import com.demcha.examples.flagships.EngineDeckExample; @@ -58,6 +59,7 @@ import com.demcha.examples.flagships.MavenBannerPptxExample; import com.demcha.examples.flagships.ModuleFirstFileExample; import com.demcha.examples.flagships.TwinOutputExample; +import com.demcha.examples.templates.coverletter.CoverLetterFileExample; import com.demcha.examples.templates.coverletter.CvBlueBannerLetterV2Example; import com.demcha.examples.templates.coverletter.CvBoxedSectionsLetterV2Example; import com.demcha.examples.templates.coverletter.CvCenteredHeadlineLetterV2Example; @@ -82,6 +84,7 @@ import com.demcha.examples.templates.cv.v2.CvEngineeringResumeExample; import com.demcha.examples.templates.cv.v2.CvExecutiveExample; import com.demcha.examples.templates.cv.v2.CvMinimalUnderlinedExample; +import com.demcha.examples.templates.cv.v2.CvMintEditorialCustomExample; import com.demcha.examples.templates.cv.v2.CvMintEditorialExample; import com.demcha.examples.templates.cv.v2.CvModernV2Example; import com.demcha.examples.templates.cv.v2.CvMonogramSidebarExample; @@ -109,6 +112,11 @@ private GenerateAllExamples() { } public static void main(String[] args) throws Exception { + // The catalogue is a statement about the code, so it starts empty: an + // example renamed or deleted must not leave its old document behind for + // the showcase to publish and the guards to count. + ExampleOutputPaths.clean(); + // === Templates === // CV / Resume (v2 layered — 16 presets, one example per preset) System.out.println("Generated: " + CvBlueBannerExample.generate()); @@ -121,6 +129,8 @@ public static void main(String[] args) throws Exception { System.out.println("Generated: " + CvExecutiveExample.generate()); System.out.println("Generated: " + CvMinimalUnderlinedExample.generate()); System.out.println("Generated: " + CvMintEditorialExample.generate()); + System.out.println("Generated: " + CvMintEditorialCustomExample.generate()); + System.out.println("Generated: " + CoverLetterFileExample.generate()); System.out.println("Generated: " + CvModernV2Example.generate()); System.out.println("Generated: " + CvMonogramSidebarExample.generate()); System.out.println("Generated: " + CvNordicCleanExample.generate()); diff --git a/examples/src/main/java/com/demcha/examples/support/ExampleOutputPaths.java b/examples/src/main/java/com/demcha/examples/support/ExampleOutputPaths.java index 4153e721f..2d022c559 100644 --- a/examples/src/main/java/com/demcha/examples/support/ExampleOutputPaths.java +++ b/examples/src/main/java/com/demcha/examples/support/ExampleOutputPaths.java @@ -1,15 +1,70 @@ package com.demcha.examples.support; +import java.io.IOException; +import java.nio.file.AccessDeniedException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Comparator; +import java.util.stream.Stream; public final class ExampleOutputPaths { private ExampleOutputPaths() { } + /** The tree every example writes into. */ + public static Path root() { + return baseDirectory().resolve("target").resolve("generated-pdfs") + .toAbsolutePath().normalize(); + } + + /** + * Empties the output tree so what remains is exactly what this run produced. + * + *

The examples only write. Rename an example's output or delete the example and + * its old file stays behind, indistinguishable from a current one — the showcase + * then publishes a document nothing produces, and the guards that read the tree + * count it as evidence. Clearing first is what makes the tree a statement about the + * code rather than about the order in which commands were run.

+ * + * @return the emptied root + */ + public static Path clean() throws IOException { + Path root = root(); + if (Files.isDirectory(root)) { + try (Stream paths = Files.walk(root)) { + for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) { + if (!path.equals(root)) { + delete(path); + } + } + } + } + Files.createDirectories(root); + return root; + } + + /** + * Deletes one entry, translating the failure a reader can actually act on. + * + *

A document held open in a viewer is the way this fails on Windows, and it is + * common enough that the runbook lists it. The raw {@code AccessDeniedException} + * arrives with a path and nothing else, at the very start of a run that has produced + * nothing yet, which reads like the generator is broken rather than like a window + * needs closing.

+ */ + private static void delete(Path path) throws IOException { + try { + Files.delete(path); + } catch (AccessDeniedException blocked) { + throw new IOException("Cannot clear " + path + " before regenerating the examples." + + " Something is holding the file open — a PDF viewer is the usual answer on" + + " Windows. Close it and run again.", blocked); + } + } + public static Path prepare(String fileName) throws Exception { - Path root = baseDirectory().resolve("target").resolve("generated-pdfs"); + Path root = root(); Files.createDirectories(root); return root.resolve(fileName).toAbsolutePath().normalize(); } @@ -26,7 +81,7 @@ public static Path prepare(String fileName) throws Exception { * @return absolute path under the categorised generated-pdfs tree */ public static Path prepare(String category, String fileName) throws Exception { - Path root = baseDirectory().resolve("target").resolve("generated-pdfs").resolve(category); + Path root = root().resolve(category); Files.createDirectories(root); return root.resolve(fileName).toAbsolutePath().normalize(); } diff --git a/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java b/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java index f67a35441..452312841 100644 --- a/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java +++ b/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java @@ -59,8 +59,16 @@ record Entry(String title, String description, List tags, String codeUrl cv("cv-monogram-sidebar-v2", "CvMonogramSidebarExample", "Monogram Sidebar", "Sidebar with monogram badge, accent rule, and structured contact + skills column.", "sidebar", "monogram"); cv("cv-minimal-underlined-v2", "CvMinimalUnderlinedExample", "Minimal Underlined", "Single-column layout with underlined section titles and tight whitespace — minimalist reference shape.", "minimal"); cv("cv-mint-editorial-v2", "CvMintEditorialExample", "Mint Editorial", "Magazine-style editorial CV with mint accent palette and two-column body.", "editorial", "mint"); + cv("cv-mint-editorial-v2-custom", "CvMintEditorialCustomExample", "Mint Editorial (custom band)", "The same preset with one colour changed through its Options — a kraft-paper masthead band, everything else left at the preset's defaults.", "editorial", "mint"); // ===== Templates / Cover Letter (v2 layered, paired 1:1 with CV) ===== + // Registered directly: letter() points at the layered preset examples under + // coverletter/v2, and this one sits a level up because it composes without a + // preset at all. + ENTRIES.put("cover-letter", entry("Cover Letter", + "One page composed straight in the canonical DSL — section presets carry the hierarchy, no template involved.", + withCategory("letter"), + EX_BASE + "/templates/coverletter/CoverLetterFileExample.java")); letter("cover-letter-modern-professional-v2", "CvModernProfessionalLetterV2Example", "Modern Professional letter", "Letter paired with the Modern Professional CV palette."); letter("cover-letter-nordic-clean-v2", "CvNordicCleanLetterV2Example", "Nordic Clean letter", "Letter paired with the Nordic Clean CV palette."); letter("cover-letter-classic-serif-v2", "CvClassicSerifLetterV2Example", "Classic Serif letter", "Letter with Times-style serif typography."); diff --git a/examples/src/main/java/com/demcha/examples/support/ShowcaseSync.java b/examples/src/main/java/com/demcha/examples/support/ShowcaseSync.java index a31ff462c..60e541103 100644 --- a/examples/src/main/java/com/demcha/examples/support/ShowcaseSync.java +++ b/examples/src/main/java/com/demcha/examples/support/ShowcaseSync.java @@ -16,7 +16,10 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.TreeMap; +import java.util.TreeSet; +import java.util.stream.Collectors; import java.util.stream.Stream; /** @@ -89,6 +92,7 @@ public static void main(String[] args) throws Exception { + ". Run GenerateAllExamples first — refusing to clear the published" + " showcase and write an empty manifest."); } + requireCompleteCatalogue(pdfs, generatedPdfs); // The copy below replaces files but never removes them, so an example that was // renamed or deleted kept its artifact published on GitHub Pages indefinitely — @@ -189,21 +193,74 @@ private static void renderPreview(Path pdfPath, Path pngTarget) throws IOExcepti } /** - * Removes every published file under a subtree, leaving the directories in place. - * Only files matter: git tracks no empty directory, and the copy below recreates - * whatever it needs. Deleting the directories too is what makes this fragile on - * Windows, where a just-closed handle can still fail the parent's removal with - * {@code AccessDeniedException}. A missing tree is a no-op — the normal case on a + * Refuses to publish a catalogue that is missing documents the register describes. + * + *

"Not empty" stopped being enough the moment generation began by clearing the + * tree. A run that dies partway now leaves exactly the documents written before the + * throw, and that passes an emptiness check — so the sync would mirror the remains, + * delete every published file the run never got to, and rewrite the manifest to + * match. The site ends up internally consistent and quietly missing half its + * examples, which is worse than an obvious failure.

+ * + *

The register is the yardstick because it is the one list that says what the + * catalogue is meant to contain. Deleting an example legitimately shrinks the + * catalogue, and that stays possible — the entry goes with it, which the coverage + * guard requires anyway.

+ */ + private static void requireCompleteCatalogue(List pdfs, Path generatedPdfs) { + Set produced = pdfs.stream() + .map(pdf -> pdf.getFileName().toString()) + .map(name -> name.substring(0, name.length() - ".pdf".length())) + .collect(Collectors.toCollection(TreeSet::new)); + + Set missing = new TreeSet<>(ShowcaseMetadata.registeredEntries().keySet()); + missing.removeAll(produced); + if (!missing.isEmpty()) { + throw new IllegalStateException( + "The generated catalogue is missing " + missing.size() + " document(s) the" + + " showcase register describes: " + missing + ". Generation starts by" + + " emptying " + generatedPdfs + ", so a run that failed partway leaves" + + " exactly what it managed to write. Re-run GenerateAllExamples and let it" + + " finish — publishing now would delete the missing documents from the site" + + " and rewrite the manifest without them. If an example was removed on" + + " purpose, remove its register entry too."); + } + } + + /** + * Empties a published subtree. A missing tree is a no-op — the normal case on a * fresh checkout. + * + *

Every file goes, and a failure there stops the sync: a published file that + * survives is a document the site still serves and the manifest no longer lists.

+ * + *

Directories are removed too, deepest first, but only where the filesystem + * allows. On Windows a just-closed handle can still fail the parent's removal with + * {@code AccessDeniedException} — observed here on + * {@code web/showcase/pdf/templates/schedule} — and an empty folder left behind is + * a cosmetic blemish on the deployed tree, invisible to git and to every reader who + * arrives through the manifest. Failing the whole publish over one would trade a + * real problem for a tidiness preference.

*/ private static void deletePublishedFiles(Path root) throws IOException { if (!Files.exists(root)) { return; } try (Stream walk = Files.walk(root)) { - List files = walk.filter(Files::isRegularFile).toList(); - for (Path file : files) { - Files.delete(file); + for (Path path : walk.sorted(Comparator.reverseOrder()).toList()) { + if (path.equals(root)) { + continue; + } + if (Files.isDirectory(path)) { + try { + Files.delete(path); + } catch (IOException locked) { + System.out.println("Left in place (the filesystem would not remove it): " + + path + " — " + locked.getClass().getSimpleName()); + } + } else { + Files.delete(path); + } } } } diff --git a/examples/src/test/java/com/demcha/examples/GeneratedCatalogue.java b/examples/src/test/java/com/demcha/examples/GeneratedCatalogue.java index 447715788..c56ed688b 100644 --- a/examples/src/test/java/com/demcha/examples/GeneratedCatalogue.java +++ b/examples/src/test/java/com/demcha/examples/GeneratedCatalogue.java @@ -1,10 +1,6 @@ package com.demcha.examples; -import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; -import java.util.Comparator; -import java.util.stream.Stream; /** * The generated example tree, produced once per JVM for whichever test asks first. @@ -13,13 +9,11 @@ * metadata that describes them — and running the whole catalogue twice would double the * cost of the examples job for nothing.

* - *

The tree is emptied before it is rebuilt. The runner only writes, so without that - * an artefact from an earlier build survives: delete an example and leave its showcase - * entry behind, run without {@code clean}, and the coverage guard matches the entry - * against yesterday's file and passes on a document the current code no longer writes. - * That is invisible to a negative test, because the negative test starts from a tree the - * runner just wrote. CI happens to be safe — it compiles clean first — which only means - * the local run is the lenient one, and the local run is where the guard is read.

+ *

The emptying belongs to {@link GenerateAllExamples} rather than to this class. The + * release script and CI run the runner directly, so a clean step that lived only here + * would leave those two paths behind — and the failure it prevents, a document surviving + * an example that no longer writes it, is exactly the kind that shows up in what gets + * published rather than in what gets tested.

* *

Public because the metadata guard sits in the {@code support} package, beside the * package-private register it reads. Test scope only — nothing here is published.

@@ -42,32 +36,13 @@ public static synchronized Path generateOnce() throws Exception { } /** - * Empties the tree and runs the whole catalogue into it. The path {@link #generateOnce} - * takes, exposed so the guard covering the emptying can drive it from a known state - * instead of depending on which test class happened to run first. + * Runs the catalogue, which empties the tree before writing it. Exposed so the guard + * covering the emptying can drive it from a known state instead of depending on + * which test class happened to run first. */ static synchronized Path regenerate() throws Exception { - clear(ROOT); - Files.createDirectories(ROOT); GenerateAllExamples.main(new String[0]); generated = true; return ROOT; } - - /** - * Deletes everything under {@code root}, deepest entry first so a directory is empty - * by the time it is removed. {@code root} itself stays. - */ - private static void clear(Path root) throws IOException { - if (!Files.isDirectory(root)) { - return; - } - try (Stream paths = Files.walk(root)) { - for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) { - if (!path.equals(root)) { - Files.delete(path); - } - } - } - } }