From 840f944bddc4c04a655957ef4da6146820bd03e1 Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Sun, 31 May 2026 12:35:28 +0100
Subject: [PATCH] chore(api): add class-level @since to public entry points +
coverage guard (H1)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Wires up Track H1 from the v1.6.5→1.7 readiness taskboard. 26 public
types in the canonical user-reached packages now carry class-level
@since 1.0.0 Javadoc tags so callers see the introduction version at
IDE quick-doc / generated Javadoc time without trawling CHANGELOG
history.
Files updated (class-level @since 1.0.0):
com.demcha.compose.GraphCompose
com.demcha.compose.document.api.DocumentSession
com.demcha.compose.document.api.DocumentPageSize
com.demcha.compose.document.api.PageBackgroundFill
com.demcha.compose.document.dsl.DocumentDsl
com.demcha.compose.document.dsl.RichText
com.demcha.compose.document.dsl.Transformable
com.demcha.compose.document.dsl.{19 *Builder.java}
Baseline @since 1.0.0 is the pragmatic choice: these are foundational
types that have shipped since the first GraphCompose release and
predate the CHANGELOG history that could pin a more specific version.
New public types added in subsequent PRs should use the upcoming
release version (e.g. @since 1.6.6).
New guard: PublicApiSinceTagCoverageTest source-scans the three
entry-point roots (GraphCompose.java, document.api/, document.dsl/)
and fails the build if a new public top-level type lands without a
class-level @since tag. internal/ sub-packages are excluded by
convention — InternalAnnotationCoverageTest covers those.
Method-level @since backfill for the ~380 public methods in these
packages is intentionally out of scope; method @since is now policy
for ALL new public methods (the senior-review skill enforces this on
new code) but retrofitting the existing surface is its own task.
Verification:
- ./mvnw -B -ntp test -pl . -Dtest=PublicApiSinceTagCoverageTest
-> 1 test, 0 failures (guard green against current state)
- ./mvnw -B -ntp test -pl .
-> full suite green, no regression from the bulk Javadoc edits
CHANGELOG entry added to v1.6.6 — Planned ### Build section.
---
CHANGELOG.md | 13 ++
.../java/com/demcha/compose/GraphCompose.java | 1 +
.../document/api/DocumentPageSize.java | 1 +
.../compose/document/api/DocumentSession.java | 1 +
.../document/api/PageBackgroundFill.java | 1 +
.../document/dsl/AbstractFlowBuilder.java | 1 +
.../compose/document/dsl/BarcodeBuilder.java | 1 +
.../document/dsl/CanvasLayerBuilder.java | 1 +
.../compose/document/dsl/DividerBuilder.java | 1 +
.../compose/document/dsl/DocumentDsl.java | 1 +
.../compose/document/dsl/EllipseBuilder.java | 1 +
.../compose/document/dsl/ImageBuilder.java | 1 +
.../document/dsl/LayerStackBuilder.java | 1 +
.../compose/document/dsl/LineBuilder.java | 1 +
.../compose/document/dsl/ListBuilder.java | 1 +
.../compose/document/dsl/ModuleBuilder.java | 1 +
.../document/dsl/PageBreakBuilder.java | 1 +
.../compose/document/dsl/PageFlowBuilder.java | 1 +
.../document/dsl/ParagraphBuilder.java | 1 +
.../demcha/compose/document/dsl/RichText.java | 1 +
.../compose/document/dsl/RowBuilder.java | 1 +
.../compose/document/dsl/SectionBuilder.java | 1 +
.../compose/document/dsl/ShapeBuilder.java | 1 +
.../document/dsl/ShapeContainerBuilder.java | 1 +
.../compose/document/dsl/SpacerBuilder.java | 1 +
.../compose/document/dsl/TableBuilder.java | 1 +
.../compose/document/dsl/Transformable.java | 1 +
.../PublicApiSinceTagCoverageTest.java | 126 ++++++++++++++++++
28 files changed, 165 insertions(+)
create mode 100644 src/test/java/com/demcha/documentation/PublicApiSinceTagCoverageTest.java
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 81a998a8..a6f19ad3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,19 @@ JitPack continue to resolve through the existing coordinates.
`./mvnw -DskipTests -P japicmp verify -pl .`; HTML/MD/XML reports
land in `target/japicmp/`. JitPack repository is scoped to the
`japicmp` profile, so downstream consumers do not inherit it.
+- **Class-level `@since 1.0.0` Javadoc on the public entry-point
+ surface** (Track H1). 26 public types in the canonical user-reached
+ packages (`com.demcha.compose.GraphCompose`, `com.demcha.compose.document.api.{DocumentSession, DocumentPageSize, PageBackgroundFill}`,
+ `com.demcha.compose.document.dsl.{DocumentDsl, RichText, Transformable}` plus all 19 DSL builders)
+ now carry class-level `@since 1.0.0` Javadoc tags so callers can see
+ the introduction version at IDE quick-doc / generated Javadoc time
+ without trawling CHANGELOG history. New guard test
+ `PublicApiSinceTagCoverageTest` source-scans the three entry-point
+ roots and fails the build if a new public top-level type lands
+ without a class-level `@since` tag; `internal/` sub-packages are
+ excluded by convention (`InternalAnnotationCoverageTest` covers those).
+ Method-level `@since` backfill for the ~380 public methods in these
+ packages is intentionally out of scope here and tracked separately.
### Engine internals (no behaviour change)
diff --git a/src/main/java/com/demcha/compose/GraphCompose.java b/src/main/java/com/demcha/compose/GraphCompose.java
index 06a9b7bc..a9841ae9 100644
--- a/src/main/java/com/demcha/compose/GraphCompose.java
+++ b/src/main/java/com/demcha/compose/GraphCompose.java
@@ -31,6 +31,7 @@
*
*
* @author Artem Demchyshyn
+ * @since 1.0.0
*
* Build a PDF file with the canonical DSL
*
diff --git a/src/main/java/com/demcha/compose/document/api/DocumentPageSize.java b/src/main/java/com/demcha/compose/document/api/DocumentPageSize.java
index 242b4997..7858f45b 100644
--- a/src/main/java/com/demcha/compose/document/api/DocumentPageSize.java
+++ b/src/main/java/com/demcha/compose/document/api/DocumentPageSize.java
@@ -8,6 +8,7 @@
*
* @param width page width in points
* @param height page height in points
+ * @since 1.0.0
*/
public record DocumentPageSize(double width, double height) {
/**
diff --git a/src/main/java/com/demcha/compose/document/api/DocumentSession.java b/src/main/java/com/demcha/compose/document/api/DocumentSession.java
index beb33139..dbbed9ef 100644
--- a/src/main/java/com/demcha/compose/document/api/DocumentSession.java
+++ b/src/main/java/com/demcha/compose/document/api/DocumentSession.java
@@ -59,6 +59,7 @@
* Thread-safety: this type is mutable and not thread-safe.
*
* @author Artem Demchyshyn
+ * @since 1.0.0
*/
public final class DocumentSession implements AutoCloseable {
private static final Logger LIFECYCLE_LOG = LoggerFactory.getLogger("com.demcha.compose.document.lifecycle");
diff --git a/src/main/java/com/demcha/compose/document/api/PageBackgroundFill.java b/src/main/java/com/demcha/compose/document/api/PageBackgroundFill.java
index 5c450cef..27abe2ea 100644
--- a/src/main/java/com/demcha/compose/document/api/PageBackgroundFill.java
+++ b/src/main/java/com/demcha/compose/document/api/PageBackgroundFill.java
@@ -44,6 +44,7 @@
* Keep {@code yRatio + heightRatio <= 1.0} so the fill
* stays within the page.
* @param color fill color (required)
+ * @since 1.0.0
*/
public record PageBackgroundFill(double xRatio,
double yRatio,
diff --git a/src/main/java/com/demcha/compose/document/dsl/AbstractFlowBuilder.java b/src/main/java/com/demcha/compose/document/dsl/AbstractFlowBuilder.java
index 4e1b195b..1e0cc527 100644
--- a/src/main/java/com/demcha/compose/document/dsl/AbstractFlowBuilder.java
+++ b/src/main/java/com/demcha/compose/document/dsl/AbstractFlowBuilder.java
@@ -23,6 +23,7 @@
*
* @param concrete builder type
* @param concrete node type
+ * @since 1.0.0
*/
public abstract class AbstractFlowBuilder, N extends DocumentNode> {
private String name = "";
diff --git a/src/main/java/com/demcha/compose/document/dsl/BarcodeBuilder.java b/src/main/java/com/demcha/compose/document/dsl/BarcodeBuilder.java
index 08c29557..94c055bf 100644
--- a/src/main/java/com/demcha/compose/document/dsl/BarcodeBuilder.java
+++ b/src/main/java/com/demcha/compose/document/dsl/BarcodeBuilder.java
@@ -42,6 +42,7 @@
/**
* Builder for semantic barcode and QR-code nodes.
+ * @since 1.0.0
*/
public final class BarcodeBuilder implements Transformable {
private String name = "";
diff --git a/src/main/java/com/demcha/compose/document/dsl/CanvasLayerBuilder.java b/src/main/java/com/demcha/compose/document/dsl/CanvasLayerBuilder.java
index 1a8db808..b07fd727 100644
--- a/src/main/java/com/demcha/compose/document/dsl/CanvasLayerBuilder.java
+++ b/src/main/java/com/demcha/compose/document/dsl/CanvasLayerBuilder.java
@@ -20,6 +20,7 @@
* positive {@code y} is down.
*
* @author Artem Demchyshyn
+ * @since 1.0.0
*/
public final class CanvasLayerBuilder {
diff --git a/src/main/java/com/demcha/compose/document/dsl/DividerBuilder.java b/src/main/java/com/demcha/compose/document/dsl/DividerBuilder.java
index fa3942df..606ce8e7 100644
--- a/src/main/java/com/demcha/compose/document/dsl/DividerBuilder.java
+++ b/src/main/java/com/demcha/compose/document/dsl/DividerBuilder.java
@@ -41,6 +41,7 @@
/**
* Builder for thin horizontal divider nodes.
+ * @since 1.0.0
*/
public final class DividerBuilder extends ShapeBuilder {
DividerBuilder() {
diff --git a/src/main/java/com/demcha/compose/document/dsl/DocumentDsl.java b/src/main/java/com/demcha/compose/document/dsl/DocumentDsl.java
index f87e2434..bfa8be3c 100644
--- a/src/main/java/com/demcha/compose/document/dsl/DocumentDsl.java
+++ b/src/main/java/com/demcha/compose/document/dsl/DocumentDsl.java
@@ -24,6 +24,7 @@
* }
*
* @author Artem Demchyshyn
+ * @since 1.0.0
*/
public final class DocumentDsl {
private final DocumentSession session;
diff --git a/src/main/java/com/demcha/compose/document/dsl/EllipseBuilder.java b/src/main/java/com/demcha/compose/document/dsl/EllipseBuilder.java
index b408ba3b..5b78fb0e 100644
--- a/src/main/java/com/demcha/compose/document/dsl/EllipseBuilder.java
+++ b/src/main/java/com/demcha/compose/document/dsl/EllipseBuilder.java
@@ -14,6 +14,7 @@
* Builder for semantic circle and ellipse nodes.
*
* @author Artem Demchyshyn
+ * @since 1.0.0
*/
public final class EllipseBuilder implements Transformable {
private String name = "";
diff --git a/src/main/java/com/demcha/compose/document/dsl/ImageBuilder.java b/src/main/java/com/demcha/compose/document/dsl/ImageBuilder.java
index 4622b7c8..8fa10b75 100644
--- a/src/main/java/com/demcha/compose/document/dsl/ImageBuilder.java
+++ b/src/main/java/com/demcha/compose/document/dsl/ImageBuilder.java
@@ -15,6 +15,7 @@
* Builder for semantic image nodes.
*
* @author Artem Demchyshyn
+ * @since 1.0.0
*/
public final class ImageBuilder implements Transformable {
private String name = "";
diff --git a/src/main/java/com/demcha/compose/document/dsl/LayerStackBuilder.java b/src/main/java/com/demcha/compose/document/dsl/LayerStackBuilder.java
index d26197a7..12273ae4 100644
--- a/src/main/java/com/demcha/compose/document/dsl/LayerStackBuilder.java
+++ b/src/main/java/com/demcha/compose/document/dsl/LayerStackBuilder.java
@@ -19,6 +19,7 @@
* block by the canonical paginator.
*
* @author Artem Demchyshyn
+ * @since 1.0.0
*/
public final class LayerStackBuilder {
private String name = "";
diff --git a/src/main/java/com/demcha/compose/document/dsl/LineBuilder.java b/src/main/java/com/demcha/compose/document/dsl/LineBuilder.java
index ddedab75..b45153ac 100644
--- a/src/main/java/com/demcha/compose/document/dsl/LineBuilder.java
+++ b/src/main/java/com/demcha/compose/document/dsl/LineBuilder.java
@@ -12,6 +12,7 @@
* Builder for fixed-size semantic line nodes.
*
* @author Artem Demchyshyn
+ * @since 1.0.0
*/
public final class LineBuilder implements Transformable {
private String name = "";
diff --git a/src/main/java/com/demcha/compose/document/dsl/ListBuilder.java b/src/main/java/com/demcha/compose/document/dsl/ListBuilder.java
index bbff5929..2ed965c1 100644
--- a/src/main/java/com/demcha/compose/document/dsl/ListBuilder.java
+++ b/src/main/java/com/demcha/compose/document/dsl/ListBuilder.java
@@ -42,6 +42,7 @@
/**
* Builder for semantic list nodes with marker and spacing controls.
+ * @since 1.0.0
*/
public final class ListBuilder {
private String name = "";
diff --git a/src/main/java/com/demcha/compose/document/dsl/ModuleBuilder.java b/src/main/java/com/demcha/compose/document/dsl/ModuleBuilder.java
index c22eaa1d..f6fdeca2 100644
--- a/src/main/java/com/demcha/compose/document/dsl/ModuleBuilder.java
+++ b/src/main/java/com/demcha/compose/document/dsl/ModuleBuilder.java
@@ -43,6 +43,7 @@
* Builder for named semantic modules with optional title content.
*
* @author Artem Demchyshyn
+ * @since 1.0.0
*/
public final class ModuleBuilder extends AbstractFlowBuilder {
private String title = "";
diff --git a/src/main/java/com/demcha/compose/document/dsl/PageBreakBuilder.java b/src/main/java/com/demcha/compose/document/dsl/PageBreakBuilder.java
index e9cfd26a..4c5eda59 100644
--- a/src/main/java/com/demcha/compose/document/dsl/PageBreakBuilder.java
+++ b/src/main/java/com/demcha/compose/document/dsl/PageBreakBuilder.java
@@ -41,6 +41,7 @@
/**
* Builder for explicit page-break control nodes.
+ * @since 1.0.0
*/
public final class PageBreakBuilder {
private String name = "";
diff --git a/src/main/java/com/demcha/compose/document/dsl/PageFlowBuilder.java b/src/main/java/com/demcha/compose/document/dsl/PageFlowBuilder.java
index 5ad1adeb..8e030180 100644
--- a/src/main/java/com/demcha/compose/document/dsl/PageFlowBuilder.java
+++ b/src/main/java/com/demcha/compose/document/dsl/PageFlowBuilder.java
@@ -43,6 +43,7 @@
* Builder for root page-flow containers that attach to a document session.
*
* @author Artem Demchyshyn
+ * @since 1.0.0
*/
public final class PageFlowBuilder extends AbstractFlowBuilder {
private final DocumentSession session;
diff --git a/src/main/java/com/demcha/compose/document/dsl/ParagraphBuilder.java b/src/main/java/com/demcha/compose/document/dsl/ParagraphBuilder.java
index e338e121..17cc9bd0 100644
--- a/src/main/java/com/demcha/compose/document/dsl/ParagraphBuilder.java
+++ b/src/main/java/com/demcha/compose/document/dsl/ParagraphBuilder.java
@@ -21,6 +21,7 @@
/**
* Builder for semantic paragraph nodes and inline text runs.
+ * @since 1.0.0
*/
public final class ParagraphBuilder {
private String name = "";
diff --git a/src/main/java/com/demcha/compose/document/dsl/RichText.java b/src/main/java/com/demcha/compose/document/dsl/RichText.java
index da5f6a11..c588375b 100644
--- a/src/main/java/com/demcha/compose/document/dsl/RichText.java
+++ b/src/main/java/com/demcha/compose/document/dsl/RichText.java
@@ -35,6 +35,7 @@
* }
*
* @author Artem Demchyshyn
+ * @since 1.0.0
*/
public final class RichText {
private final List runs = new ArrayList<>();
diff --git a/src/main/java/com/demcha/compose/document/dsl/RowBuilder.java b/src/main/java/com/demcha/compose/document/dsl/RowBuilder.java
index c426dbce..03a0c699 100644
--- a/src/main/java/com/demcha/compose/document/dsl/RowBuilder.java
+++ b/src/main/java/com/demcha/compose/document/dsl/RowBuilder.java
@@ -44,6 +44,7 @@
* atomic pagination.
*
* @author Artem Demchyshyn
+ * @since 1.0.0
*/
public final class RowBuilder {
private String name = "";
diff --git a/src/main/java/com/demcha/compose/document/dsl/SectionBuilder.java b/src/main/java/com/demcha/compose/document/dsl/SectionBuilder.java
index 8d6ae40f..9f737506 100644
--- a/src/main/java/com/demcha/compose/document/dsl/SectionBuilder.java
+++ b/src/main/java/com/demcha/compose/document/dsl/SectionBuilder.java
@@ -43,6 +43,7 @@
* Builder for semantic sections inside document flows.
*
* @author Artem Demchyshyn
+ * @since 1.0.0
*/
public final class SectionBuilder extends AbstractFlowBuilder {
/**
diff --git a/src/main/java/com/demcha/compose/document/dsl/ShapeBuilder.java b/src/main/java/com/demcha/compose/document/dsl/ShapeBuilder.java
index 923cbc67..2e137a86 100644
--- a/src/main/java/com/demcha/compose/document/dsl/ShapeBuilder.java
+++ b/src/main/java/com/demcha/compose/document/dsl/ShapeBuilder.java
@@ -45,6 +45,7 @@
* Builder for rectangle-like semantic shape nodes.
*
* @author Artem Demchyshyn
+ * @since 1.0.0
*/
public class ShapeBuilder implements Transformable {
protected String name = "";
diff --git a/src/main/java/com/demcha/compose/document/dsl/ShapeContainerBuilder.java b/src/main/java/com/demcha/compose/document/dsl/ShapeContainerBuilder.java
index 1ad370f7..c63dfc32 100644
--- a/src/main/java/com/demcha/compose/document/dsl/ShapeContainerBuilder.java
+++ b/src/main/java/com/demcha/compose/document/dsl/ShapeContainerBuilder.java
@@ -27,6 +27,7 @@
* nine {@link LayerAlign} anchors plus optional on-screen offset.
*
* @author Artem Demchyshyn
+ * @since 1.0.0
*/
public final class ShapeContainerBuilder implements Transformable {
private String name = "";
diff --git a/src/main/java/com/demcha/compose/document/dsl/SpacerBuilder.java b/src/main/java/com/demcha/compose/document/dsl/SpacerBuilder.java
index e051bf94..5b72ea48 100644
--- a/src/main/java/com/demcha/compose/document/dsl/SpacerBuilder.java
+++ b/src/main/java/com/demcha/compose/document/dsl/SpacerBuilder.java
@@ -7,6 +7,7 @@
* Builder for invisible fixed-size spacer nodes.
*
* @author Artem Demchyshyn
+ * @since 1.0.0
*/
public final class SpacerBuilder {
private String name = "";
diff --git a/src/main/java/com/demcha/compose/document/dsl/TableBuilder.java b/src/main/java/com/demcha/compose/document/dsl/TableBuilder.java
index 888bbc82..dc292210 100644
--- a/src/main/java/com/demcha/compose/document/dsl/TableBuilder.java
+++ b/src/main/java/com/demcha/compose/document/dsl/TableBuilder.java
@@ -24,6 +24,7 @@
/**
* Builder for semantic table nodes.
+ * @since 1.0.0
*/
public final class TableBuilder {
private String name = "";
diff --git a/src/main/java/com/demcha/compose/document/dsl/Transformable.java b/src/main/java/com/demcha/compose/document/dsl/Transformable.java
index e56ebd75..e5330978 100644
--- a/src/main/java/com/demcha/compose/document/dsl/Transformable.java
+++ b/src/main/java/com/demcha/compose/document/dsl/Transformable.java
@@ -22,6 +22,7 @@
* can chain naturally
*
* @author Artem Demchyshyn
+ * @since 1.0.0
*/
public interface Transformable> {
diff --git a/src/test/java/com/demcha/documentation/PublicApiSinceTagCoverageTest.java b/src/test/java/com/demcha/documentation/PublicApiSinceTagCoverageTest.java
new file mode 100644
index 00000000..09883a38
--- /dev/null
+++ b/src/test/java/com/demcha/documentation/PublicApiSinceTagCoverageTest.java
@@ -0,0 +1,126 @@
+package com.demcha.documentation;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Asserts every {@code public} type in the canonical authoring entry-point
+ * packages carries a class-level {@code @since} Javadoc tag. The guard is
+ * intentionally narrow — it covers only the surfaces a user first reaches
+ * for ({@link com.demcha.compose.GraphCompose} factory, the
+ * {@code document.api} session / builder seam, and the {@code document.dsl}
+ * authoring builders) — not the whole public surface. A broader sweep
+ * across {@code document.node}, {@code document.style}, and the template
+ * packages is tracked separately.
+ *
+ * The check is source-level rather than reflective because
+ * {@code @since} is a Javadoc tag, not a runtime annotation. The guard
+ * scans each {@code *.java} file and verifies that an {@code @since}
+ * token appears somewhere before the first {@code public class},
+ * {@code public final class}, {@code public sealed class}, {@code public
+ * abstract class}, {@code public interface}, {@code public sealed
+ * interface}, {@code public record}, {@code public final record}, or
+ * {@code public enum} declaration.
+ *
+ * Files with no public top-level type ({@code package-info.java} and
+ * the like) are skipped.
+ */
+class PublicApiSinceTagCoverageTest {
+
+ private static final Path PROJECT_ROOT = Paths.get(".").toAbsolutePath().normalize();
+
+ /**
+ * The narrow set of "entry-point" sources this guard scans. New
+ * packages should be added here only when they qualify as primary
+ * user-reached surface; otherwise leave them for the broader sweep
+ * tracked under the v1.6.6 H-track.
+ */
+ private static final List ROOTS = List.of(
+ PROJECT_ROOT.resolve("src/main/java/com/demcha/compose/GraphCompose.java"),
+ PROJECT_ROOT.resolve("src/main/java/com/demcha/compose/document/api"),
+ PROJECT_ROOT.resolve("src/main/java/com/demcha/compose/document/dsl")
+ );
+
+ /**
+ * Files explicitly excused from the {@code @since} requirement.
+ * Add an entry here only when the file truly does not declare a
+ * public top-level type that callers can reach — e.g. an internal
+ * helper that ended up in an entry-point package by accident and
+ * is on its way out.
+ */
+ private static final Set ALLOWLIST = Set.of();
+
+ private static final Pattern FIRST_PUBLIC_TYPE = Pattern.compile(
+ "(?m)^public\\s+(?:final\\s+|abstract\\s+|sealed\\s+|non-sealed\\s+)*"
+ + "(?:class|interface|record|enum|@interface)\\b");
+
+ @Test
+ void publicEntryPointFilesCarryClassLevelSinceTag() throws IOException {
+ List missing = new ArrayList<>();
+ for (Path root : ROOTS) {
+ scan(root, missing);
+ }
+ assertThat(missing)
+ .as("public entry-point files missing class-level @since tag")
+ .isEmpty();
+ }
+
+ private void scan(Path root, List missing) throws IOException {
+ if (!Files.exists(root)) {
+ // Source layout change — let the test fail loudly so the
+ // maintainer notices the dropped root rather than silently
+ // skipping coverage.
+ throw new IllegalStateException("Guard root does not exist: " + root);
+ }
+ if (Files.isRegularFile(root)) {
+ checkFile(root, missing);
+ return;
+ }
+ try (Stream paths = Files.walk(root)) {
+ paths.filter(Files::isRegularFile)
+ .filter(p -> p.getFileName().toString().endsWith(".java"))
+ .filter(p -> !p.getFileName().toString().equals("package-info.java"))
+ // Files inside a `.internal` subpackage are internal by
+ // package-naming convention; treat the same way as the
+ // package-level @Internal annotation in `document.layout`.
+ // Coverage on those packages is enforced separately by
+ // InternalAnnotationCoverageTest.
+ .filter(p -> !p.toString().replace('\\', '/').contains("/internal/"))
+ .forEach(p -> checkFile(p, missing));
+ }
+ }
+
+ private void checkFile(Path file, List missing) {
+ String relative = PROJECT_ROOT.relativize(file).toString().replace('\\', '/');
+ if (ALLOWLIST.contains(relative)) {
+ return;
+ }
+ String content;
+ try {
+ content = Files.readString(file);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to read " + file, e);
+ }
+ Matcher m = FIRST_PUBLIC_TYPE.matcher(content);
+ if (!m.find()) {
+ // Source file has no public top-level type — skip silently.
+ return;
+ }
+ String beforeFirstPublicType = content.substring(0, m.start());
+ if (!beforeFirstPublicType.contains("@since")) {
+ missing.add(relative);
+ }
+ }
+}