Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ follow semantic versioning; release dates are ISO 8601.
- **`graph-compose-render-pptx` declares PDFBox.** It compiles against
`org.apache.pdfbox` types while declaring only `fontbox`, taking the rest
transitively; the resolved version is unchanged.
- **The contributor guide stops teaching a removed architecture.** The engine
implementation guide still walked a new object through attaching components to an
entity, adding a render marker and a container-growth marker, and implementing
`Breakable` — a model 2.0 removed, and one the current contributing guide
contradicts. It is archived, with a banner naming what replaced it; the extension
guide and the package map are now the route for anyone adding a node, a handler or
a backend.
- **Documentation drift fails the build.** Three checks now cover what four
releases of hand-fixing kept re-breaking: a relative link in the public docs
must resolve, a contributor-facing document must not name a type 2.0 removed,
and a published snippet that names a `*_BOLD` font constant must pair it with a
decoration — the constant resolves to its base family, so without the decoration
the text renders regular. The README release-status block must name a published
version and link the tag it names. `SECURITY.md`, `SUPPORT.md`, `ROADMAP.md` and
`.github/` are scanned for the first time; historical records are skipped by path,
so a new archived page is covered the day it lands.
- **The release publishes the showcase it just built.** `cut-release.ps1` never
ran `GenerateAllExamples`, so the site was synced from whatever happened to be
in `examples/target/generated-pdfs` — nothing at all on a clean checkout, which
Expand Down
11 changes: 5 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@ Read these files first:
- [docs/architecture/package-map.md](./docs/architecture/package-map.md) — what lives in which package, and which of them are internal
- [docs/operations/benchmarks.md](./docs/operations/benchmarks.md) when you touch benchmark tooling, render hot paths, layout hot paths, or performance-facing docs

[docs/contributing/implementation-guide.md](./docs/contributing/implementation-guide.md)
goes deeper on the engine, but parts of it still describe the execution layer that
2.0 removed — read `overview.md` and `package-map.md` first and treat the guide as
background until it is rewritten.
[docs/contributing/extension-guide.md](./docs/contributing/extension-guide.md)
walks the extension points end to end — a new semantic node, a fluent setter, a
render handler, a whole backend — and is the guide to follow when adding one.

They explain the current public surface, the engine/template split, and the recommended extension points.

Expand Down Expand Up @@ -320,7 +319,7 @@ For text-heavy primitives, also read:

- [TextMeasurementSystem.java](./core/src/main/java/com/demcha/compose/engine/measurement/TextMeasurementSystem.java)
- [docs/architecture/overview.md](./docs/architecture/overview.md)
- [docs/contributing/implementation-guide.md](./docs/contributing/implementation-guide.md)
- [docs/contributing/extension-guide.md](./docs/contributing/extension-guide.md)

If the primitive should be available to application developers,
expose it through `DocumentDsl` and a public `DocumentNode`, not a
Expand Down Expand Up @@ -364,7 +363,7 @@ If a change affects resolved geometry, pagination, or ordering, prefer adding or
- Keep [README.md](./README.md) aligned with the tested examples.
- Keep benchmark values clearly dated when they are refreshed.
- Keep `assets/readme/*` screenshots consistent with the current render outputs.
- If you add a new extension point or contribution pattern, update [README.md](./README.md), [docs/architecture/overview.md](./docs/architecture/overview.md), and [docs/contributing/implementation-guide.md](./docs/contributing/implementation-guide.md) as part of the same change.
- If you add a new extension point or contribution pattern, update [README.md](./README.md), [docs/architecture/overview.md](./docs/architecture/overview.md), and [docs/contributing/extension-guide.md](./docs/contributing/extension-guide.md) as part of the same change.
- If you change benchmark flow, benchmark artifact layout, or diff selection rules, update [README.md](./README.md) and [docs/operations/benchmarks.md](./docs/operations/benchmarks.md) in the same change.
- Visual PDF artifacts are grouped under `target/visual-tests/clean/*` and `target/visual-tests/guides/*` so guide-line renders are easy to find separately from clean outputs.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.function.Predicate;
import java.util.stream.Collectors;

Expand All @@ -26,6 +28,62 @@ class CanonicalSurfaceGuardTest {
"ModuleYml",
"ModuleSummary");

/**
* Types the 2.0 line removed. Every one is absent from every {@code src/main}
* tree, so a contributor-facing document naming one is teaching code that cannot
* compile — which is how the template-authoring and engine-primitive sections of
* CONTRIBUTING went stale for a full release cycle.
*
* <p>Deliberately absent: {@code BusinessTheme} survives as an examples-local
* theme helper used by twenty-one example sources, and {@code PptxSemanticBackend}
* still ships beside the fixed-layout PPTX backend. Forbidding either would fail
* the build on text that is correct.</p>
*/
private static final List<String> RETIRED_IN_2_0_TOKENS = List.of(
"InvoiceTemplateV2",
"ProposalTemplateV2",
"WeeklyScheduleTemplateV1",
"EntityBounds",
"ParentContainerUpdater",
"ParentComponent",
"Breakable",
"hasRender(",
"CvSpec",
"CvBuilder");

/**
* Types a contributor must not be pointed at when told how to build against the
* library, <em>even though the name still resolves somewhere in this repository</em>.
*
* <p>{@code BusinessTheme} is the case that motivated the list: 2.0 removed it from
* the published surface, but the examples keep a local helper of the same name, so a
* repository-wide existence check cannot tell "this type is gone" from "this type is
* an example's private business". The distinction that matters to a reader is
* whether the type ships in a Maven artifact, and for API guidance it does not.</p>
*
* <p>Scanned over API guidance only — issue templates, the contributing guide, the
* template docs — and deliberately not over {@code examples/README.md}, which
* documents that local helper correctly.</p>
*/
private static final List<String> FORBIDDEN_IN_API_GUIDANCE = List.of(
"BusinessTheme");

/**
* Documents whose job is to record history. A migration guide, an ADR, an archived
* page or the changelog names retired surface on purpose, so the retired-token scan
* skips them by path rather than by a file list — a new historical page is then
* covered the day it is added, instead of failing the build until someone
* remembers to allowlist it.
*/
private static final List<String> HISTORICAL_RECORD_PREFIXES = List.of(
"CHANGELOG.md",
"docs/adr/",
"docs/archive/",
"docs/migration/",
"docs/private/",
"docs/roadmaps/",
"docs/templates/v1-classic/");

private static final Set<String> MAIN_CANONICAL_SOURCE_ALLOWLIST = Set.of();

private static final Set<String> DOCUMENTATION_ALLOWLIST = Set.of(
Expand Down Expand Up @@ -117,6 +175,142 @@ void publicMarkdownDocsShouldAvoidLegacySurfaceOutsideHistoricalAuditNotes() thr
PUBLIC_MARKDOWN_ALLOWLIST);
}

/**
* The contributor-facing surface must not teach types 2.0 removed.
*
* <p>Scans wider than the legacy check above: {@code SECURITY.md},
* {@code SUPPORT.md}, {@code ROADMAP.md} and {@code .github/} sit outside every
* existing guard, which is why an issue template could route reporters to a theme
* class that no longer exists and a pull-request template could offer a lane the
* repository dropped.</p>
*/
@Test
void contributorFacingDocsShouldNotNameSurfaceRetiredIn2_0() throws IOException {
List<Path> roots = List.of(
PROJECT_ROOT.resolve("README.md"),
PROJECT_ROOT.resolve("CONTRIBUTING.md"),
PROJECT_ROOT.resolve("SECURITY.md"),
PROJECT_ROOT.resolve("SUPPORT.md"),
PROJECT_ROOT.resolve("ROADMAP.md"),
PROJECT_ROOT.resolve("examples/README.md"),
PROJECT_ROOT.resolve("docs"),
PROJECT_ROOT.resolve(".github"));

Set<String> violations = new TreeSet<>();
for (Path root : roots) {
for (Path doc : markdownUnder(root)) {
String rel = relative(doc);
if (isHistoricalRecord(rel) || PUBLIC_MARKDOWN_ALLOWLIST.contains(rel)) {
continue;
}
String source = Files.readString(doc);
RETIRED_IN_2_0_TOKENS.stream()
.filter(source::contains)
.forEach(token -> violations.add(rel + " names " + token));
}
}

assertThat(violations)
.describedAs("these documents name a type 2.0 removed, so anyone following "
+ "them writes code that does not compile. A document whose purpose "
+ "is to record the removal belongs under one of %s.",
HISTORICAL_RECORD_PREFIXES)
.isEmpty();
}

/**
* API guidance must not name a type that no longer ships, even when the name still
* resolves inside this repository.
*
* <p>Separate from the retired-token scan because the question is different: not
* "does this identifier exist anywhere" but "can a reader of a published artifact
* use it". {@code BusinessTheme} answers yes to the first and no to the second,
* which is exactly how an issue template came to offer it as the theming entry
* point months after 2.0 removed it.</p>
*/
@Test
void apiGuidanceShouldNotOfferTypesThatNoLongerShip() throws IOException {
List<Path> roots = List.of(
PROJECT_ROOT.resolve("README.md"),
PROJECT_ROOT.resolve("CONTRIBUTING.md"),
PROJECT_ROOT.resolve("docs/templates"),
PROJECT_ROOT.resolve("docs/getting-started.md"),
PROJECT_ROOT.resolve("docs/first-document.md"),
PROJECT_ROOT.resolve(".github"));

Set<String> violations = new TreeSet<>();
for (Path root : roots) {
for (Path doc : markdownUnder(root)) {
String rel = relative(doc);
if (isHistoricalRecord(rel) || PUBLIC_MARKDOWN_ALLOWLIST.contains(rel)) {
continue;
}
String source = Files.readString(doc);
FORBIDDEN_IN_API_GUIDANCE.stream()
.filter(source::contains)
.forEach(token -> violations.add(rel + " offers " + token));
}
}

assertThat(violations)
.describedAs("these documents tell a reader to build against a type that no "
+ "longer ships in any published artifact. A name that survives as an "
+ "examples-local helper is still unusable by a consumer.")
.isEmpty();
}

/**
* Every relative link in the public documentation resolves on disk.
*
* <p>Needs no token list and cannot go stale: it reads what the documents actually
* point at. Renaming a test, archiving a page or deleting an example breaks the
* links to it here rather than for a reader.</p>
*/
@Test
void publicMarkdownLinksShouldResolve() throws IOException {
List<Path> roots = List.of(
PROJECT_ROOT.resolve("README.md"),
PROJECT_ROOT.resolve("CONTRIBUTING.md"),
PROJECT_ROOT.resolve("SECURITY.md"),
PROJECT_ROOT.resolve("SUPPORT.md"),
PROJECT_ROOT.resolve("ROADMAP.md"),
PROJECT_ROOT.resolve("examples/README.md"),
PROJECT_ROOT.resolve("docs"),
// Issue and pull-request templates carry relative links out of
// .github/ISSUE_TEMPLATE/, two levels deep — the shape most likely
// to break silently when a target moves.
PROJECT_ROOT.resolve(".github"));

Pattern link = Pattern.compile("\\]\\(([^)\\s]+)\\)");
Set<String> broken = new TreeSet<>();
for (Path root : roots) {
for (Path doc : markdownUnder(root)) {
if (isHistoricalRecord(relative(doc))) {
continue;
}
Matcher matcher = link.matcher(Files.readString(doc));
while (matcher.find()) {
String target = matcher.group(1);
if (target.startsWith("http") || target.startsWith("mailto:") || target.startsWith("#")) {
continue;
}
String file = target.split("#", 2)[0];
if (file.isEmpty()) {
continue;
}
if (!Files.exists(doc.getParent().resolve(file).normalize())) {
broken.add(relative(doc) + " -> " + target);
}
}
}
}

assertThat(broken)
.describedAs("a relative link in the public docs points at a file that does "
+ "not exist; the reader gets a 404 on GitHub")
.isEmpty();
}

@Test
void publicAuthoringDocsAndExamplesShouldNotImportEngineInternals() throws IOException {
assertNoForbiddenAuthoringImports(
Expand All @@ -140,6 +334,26 @@ void semanticAuthoringValuePackagesShouldNotImportEngineInternals() throws IOExc
PROJECT_ROOT.resolve("core/src/main/java/com/demcha/compose/document/image")));
}

/** Markdown files under a root, or the root itself when it is one. */
private static List<Path> markdownUnder(Path root) throws IOException {
if (Files.isRegularFile(root)) {
return root.toString().endsWith(".md") ? List.of(root) : List.of();
}
if (!Files.isDirectory(root)) {
return List.of();
}
try (var paths = Files.walk(root)) {
return paths.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".md"))
.sorted()
.toList();
}
}

private static boolean isHistoricalRecord(String relativePath) {
return HISTORICAL_RECORD_PREFIXES.stream().anyMatch(relativePath::startsWith);
}

private void assertNoForbiddenReferences(Path root, Set<String> allowlist) throws IOException {
assertNoForbiddenReferences(root, path -> true, allowlist);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,38 @@ void readmeBannerVersionDerivesFromProjectVersion() throws Exception {
.isFalse();
}

/**
* The README release-status block advertises a version that exists.
*
* <p>It used to be a hand-edit no test covered, and the release script demanded it
* already name the version being cut — so between releases {@code develop} carried
* an unpublished version as "latest stable" behind a tag URL that 404s. The script
* rewrites the block now; this pins the result, and pins the link to the text so a
* half-updated block cannot pass.</p>
*/
@Test
void readmeReleaseStatusNamesAPublishedVersion() throws Exception {
Set<String> targets = acceptableTargets();
String readme = Files.readString(PROJECT_ROOT.resolve("README.md"));

Matcher stable = Pattern.compile(
"\\*\\*Latest stable\\*\\*:\\s*\\[v(\\d+\\.\\d+\\.\\d+)]\\(([^)]+)\\)")
.matcher(readme);
assertThat(stable.find())
.describedAs("README must carry a '**Latest stable**: [vX.Y.Z](…)' release-status "
+ "line — cut-release.ps1 rewrites it during the cut and aborts without it")
.isTrue();

assertThat(stable.group(1))
.describedAs("README 'Latest stable' must name a published release, not the one "
+ "in development: its tag page does not exist until the cut (one of %s)", targets)
.isIn(targets);
assertThat(stable.group(2))
.describedAs("the 'Latest stable' link must point at the tag it names, or the badge "
+ "sends readers to a different release than the text claims")
.endsWith("/releases/tag/v" + stable.group(1));
}

@Test
void readmeInstallSnippetsMatchTheProjectVersion() throws Exception {
Set<String> targets = acceptableTargets();
Expand Down
4 changes: 2 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ back here.
| **Designer / author** wanting a custom visual style for CVs | [Templates v2 (layered) — authoring presets](templates/v2-layered/authoring-presets.md) |
| **Maintainer of a pre-2.0 caller** (classic `*Spec` + builder templates, removed in 2.0) | [Which template system? — migration map](templates/which-template-system.md) |
| **Contributor adding a new template family** to the library | [Templates v2 (layered) — contributor guide](templates/v2-layered/contributor-guide.md) |
| **Contributor extending the engine** (new node type, new backend handler) | [Extension guide](contributing/extension-guide.md) → [Implementation guide](contributing/implementation-guide.md) |
| **Contributor extending the engine** (new node type, new backend handler) | [Extension guide](contributing/extension-guide.md) → [Package map](architecture/package-map.md) |
| **Operator** running GraphCompose in production | [Production rendering](operations/production-rendering.md) → [Performance](operations/performance.md) → [Logging](operations/logging.md) |

---
Expand Down Expand Up @@ -59,7 +59,7 @@ back here.

### Contributing
- **[contributing/extension-guide.md](contributing/extension-guide.md)** — add a new node type, backend handler, or theme primitive.
- **[contributing/implementation-guide.md](contributing/implementation-guide.md)** — internal engine notes for contributors hacking on layout / measurement / pagination.
- **[architecture/pagination-ordering.md](architecture/pagination-ordering.md)** — how nodes are paginated and ordered, for contributors working on layout / measurement.
- **[contributing/release-process.md](contributing/release-process.md)** — versioning, tag procedure, Maven Central publication.

### Migrations & roadmap
Expand Down
Loading
Loading