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); + } + } +}