This is the architecture documentation of hamcrest-matcher-generator. It follows the arc42 template: twelve chapters that go from the goals and the context of the system through its building blocks, its runtime behaviour and its deployment to the decisions, quality requirements and risks that shaped it.
Version 5.1.0, built 2026-11-08.
The structure comes from arc42, the template for the documentation of software and system architecture, created and maintained by Dr. Peter Hruschka and Dr. Gernot Starke and contributors. Thanks to them for making it freely available.
1. Introduction and Goals
hamcrest-matcher-generator is an annotation processor for Java and Kotlin projects. A test developer names the model
classes (beans and records) in a @GenerateMatchers annotation, and the processor generates type-safe Hamcrest matchers
and AssertJ assertions for their properties while the project compiles. The model classes stay untouched. This chapter
states what the system has to do, which qualities matter most and who cares about it.
1.1. Requirements Overview
-
Generate Hamcrest matchers for model/domain classes at compile time
-
No production-code pollution - configuration uses one annotation on a separate configuration class, leaving model classes untouched (the retention rule is in chapter 4, Non-intrusiveness)
-
Compile-safe alternative to Hamcrest’s
hasProperty()/HasPropertyWithValue -
Informative mismatch messages via the generated fluent API
-
Lombok-annotated classes are supported
-
Works with plain Java and Kotlin (via KAPT)
Version 6.0.0 adds a second backend for AssertJ next to Hamcrest. Both are driven by the same configuration. Its features are:
-
Completeness check (the lead feature, for both backends): every property of a type is either checked or consciously ignored. Users opt in per assertion and the check runs when the test runs: Hamcrest matchers offer
checkingAllProperties(), AssertJ assertions offer the scopesatisfiesCheckingAllProperties(…)and the closing callhasAllPropertiesChecked(), andignoringFoo()marks a property as consciously ignored. Without the opt-in, matchers behave as in 5.x. -
Reference-object matcher (Hamcrest): compares a whole object against a reference object and allows single properties to be overridden or reset. The comparison goes deep: a nested value whose type has a generated matcher is compared property by property, a list item by item in order, any other collection in any order, and a map key by key. Everything else is compared with
equals. AssertJ users get the same with AssertJ’s built-in recursive comparison, which the documentation shows side by side. -
Readable mismatches (Hamcrest): only the failing checks are listed, one per line, each with its property path and both values, for example
customer.address.city: expected "Berlin" but was "Paris". Paths reach into lists (lines[1].quantity) and maps (bySku["B-2"].quantity); missing and unexpected items or keys get their own line. A generated matcher used inside one of Hamcrest’s own matchers, such ascontains(…), prints on a single line. Values of generated types are shown with their properties instead of theirtoString(). -
Collection matchers (Hamcrest runtime):
itemsInOrder(…),itemsInAnyOrder(…)andentries(…)check every item of a list or collection, or every entry of a map, and produce the paths above. Hamcrest’s own collection matchers keep working but print Hamcrest’s less specific messages. -
AssertJ messages keep AssertJ’s native
Expecting … to be … but was …style and name the property path from the asserted object, for exampleExpecting Order.customer.address.city to be: "Berlin". Inside a collection or map, the path starts again at the element type, because AssertJ reports the element’s index itself. An optional generatedRepresentationshows generated types with their properties in AssertJ’s own messages. -
Exclude configuration: packages or classes can be excluded from generation, for every backend.
-
Null-safety annotations (JSpecify) in the generated code and the Hamcrest runtime, which mainly makes the API type-safe for Kotlin.
-
Clear compiler diagnostics: errors for unresolvable names and for generated names that clash (method names with inherited ones, the top level class names the AssertJ backend generates into one package, the scope names of the Kotlin DSL), warnings for configured types without properties and for exclusions that match nothing.
-
Kotlin support: correct detection of Kotlin boolean and
is-prefixed properties, and a Kotlin DSL. -
Java module names: every published jar declares an
Automatic-Module-Name. -
AssertJ assertions with
hasFoo,hasFooSatisfying,isFoo/isNotFoo, anInstanceOfAssertFactoryper type and soft-assertion support.
After 6.0.0 no further features are planned; the project receives only fixes and automated dependency updates.
1.2. Quality Goals
| Priority | Quality | Motivation | Scenarios |
|---|---|---|---|
1 |
Correctness |
Generated assertions (Hamcrest matchers and AssertJ assertions) must be type-safe, compile without errors and fail exactly when the checked object is wrong: no false green tests - broken generation is worse than no generation |
|
2 |
Non-intrusiveness |
The core design promise: the model classes stay untouched and the generator adds no runtime dependency to production code; the annotation has |
|
3 |
Usability |
The generated assertions (Hamcrest matchers and AssertJ assertions) must be discoverable and ergonomic for test writers |
|
4 |
Build-tool agnosticism |
Works via standard APT (javac) and KAPT (kotlinc); no coupling to a specific build plugin |
|
5 |
Maintainability |
The generator codebase must remain understandable and extensible for contributors |
1.3. Stakeholders
| Role/Name | Contact | Expectations |
|---|---|---|
Test engineers / Java-Kotlin developers |
GitHub Issues |
Stable, backwards-compatible releases; correct and ergonomic generated matchers; reliable Maven Central artifacts |
Contributors (incl. maintainer marmer) |
Understandable architecture; clear contribution path; well-tested generator code |
2. Architecture Constraints
| Constraint | Background |
|---|---|
Java 17 baseline |
The processor, the runtime artifacts and the generated code run on Java 17 and later, so records are always supported (see ADR 0005). |
5.x source compatibility |
Tests written against 5.x keep compiling after a version bump alone, provided the project already builds on JDK 17 or later (users on Java 11 stay on 5.x): same matcher class names, packages and |
Apache-2.0 license |
Every published artifact stays under the Apache License 2.0, with a short |
Documentation built on JDK 21 or later |
The documentation module renders this documentation with Structurizr, whose current libraries need Java 21. A JDK 17 build skips the module (see ADR 0003). |
Conventional Commits |
The title of every pull request follows Conventional Commits, because the squash-merged title is the commit that release-please reads to decide the next version (see chapter 7). |
3. Context and Scope
3.1. Business Context
Test developers configure the types they need assertions for. Maintainers only appear through the release pipeline (see chapter 7).
| Neighbour | Exchange |
|---|---|
Test developer |
Annotates a configuration class with |
Maintainer |
Merges the Release PR. Everything after that, from the build to the publication and the announcement, runs on GitHub Actions (see chapter 7). |
Java compiler (javac, KAPT) |
Runs the processor in every round and receives the generated sources. |
Hamcrest, AssertJ |
The assertion libraries that the generated matchers and assertions build on. The generated code needs them on the user’s test class path. The AssertJ backend itself does not use AssertJ while it generates; the Hamcrest backend does use Hamcrest. |
Maven Central, GitHub Pages |
Where users find the artifacts and the documentation. |
3.2. Technical Context
| Neighbour | Channel | Direction |
|---|---|---|
Java compiler (javac, KAPT) |
Annotation processing API ( |
The compiler invokes the processor; the processor writes sources. |
Kotlin (KAPT only) |
Directory named by the processor option |
The processor writes Kotlin sources (see ADR 0004). |
Hamcrest |
The generated matchers and the runtime library extend |
Compile and test classpath of the user’s project. |
Maven Central |
Maven coordinates |
Users resolve the artifacts (see chapter 7). |
4. Solution Strategy
The solution rests on a few fundamental decisions. The table connects each quality goal of chapter 1 with the approach that serves it and with the place in this documentation where the approach is described.
| Goal | Approach | Details |
|---|---|---|
Correctness |
Generated code is checked at three levels. Golden-file tests compare the generated sources character for character, the end-to-end modules apply the processor to real beans and records and use the generated assertions in ordinary tests, and a JaCoCo floor and PIT mutation testing guard the tests themselves. The generated code is plain Java or Kotlin that the compiler checks, so a wrong property name is a compile error, not a test failure. |
Chapter 8 (CI gates), chapter 10 |
The configuration annotation sits on a separate configuration class, so model classes stay untouched and no runtime
dependency reaches production code. Both configuration annotations, |
||
Usability |
The generated API is fluent and follows one naming scheme: |
|
Build-tool agnosticism |
The processor uses only the standard annotation processing API of |
|
Maintainability |
One library-independent core builds a property model of each type. Every assertion library is a backend behind a small
interface ( |
Further decisions that shape the whole system:
-
Generate code instead of reflecting. Assertions are generated while the project compiles, with JavaPoet for Java and KotlinPoet for Kotlin. Nothing is looked up by name at test time, and the IDE can navigate and complete the generated classes.
-
Stay compatible with 5.x. Tests written against 5.x keep compiling after a version bump. Class names and the
isX()/withFoo/resetFooAPI are kept, the old annotation stays as a deprecated alias and bridge jars cover the old artifact names (ADR 0002). -
Keep the baseline modern, and the project maintainable without effort. The baseline is Java 17 (ADR 0005), so that dependency updates by Renovate are not blocked by an old Java version. Releases are cut by release-please and published from CI (chapter 7). After 6.0.0 no features are planned, only fixes and updates.
-
Document as code. This documentation and the user guide are built with the project. Code in the guide comes from example modules that compile and run in the same build, and the diagrams are views of one Structurizr model (ADR 0003).
5. Building Block View
5.1. Whitebox Overall System
- Motivation
-
The processor is split so that everything that is independent of an assertion library lives once, in the core, while every assertion library is a backend that plugs in from the outside. A user chooses libraries by choosing artifacts (see ADR 0001).
- Contained Building Blocks
| Building block | Responsibility |
|---|---|
|
The configuration annotation |
|
The processor entry point, the discovery of configured types, the library-free |
|
The Hamcrest backend: it turns a |
|
The AssertJ backend: it turns a |
|
The opt-in Kotlin DSL backend (ADR 0004). It writes one |
|
Bridge jars without code that depend on |
|
Apply the processor to real beans and records, in plain Java, Kotlin and mixed projects, and exercise the generated matchers in ordinary tests. |
|
Documentation examples for the guide and README: Maven Java for each backend, Maven Kotlin with KAPT and the Kotlin DSL,
and a Gradle build outside the reactor with its own wrapper. They are never published; the Gradle build resolves the
reactor’s artifacts from |
|
Renders this documentation to HTML and checks its internal links. It is built only on JDK 21 or later and is not published to Maven Central. |
- Important Interfaces
-
GeneratorBackendis the only interface between the core and a backend. A backend receives aTypeModeland aGenerationContext(generator name, generation time, whether JSpecify resolves at compile time, the models of all types generated in the run, so that generated code can refer to the code of another type, the names of the methods of a class on the compile class path,inheritedMethodNames,reportErrorfor a compiler error at the annotation, see chapter 8, whether Kotlin output is available,kotlinOutputAvailable,reportNotefor a compiler note,reportVerboseNotefor a compiler note that is only printed withmatchergenerator.verbose,classExistsOutsideOwnOutputfor whether a class is on the class path without being in this compilation’s own output, andreportWarningfor a compiler warning) and returns itsGeneratedSourcefiles, each a Java or Kotlin file (the Hamcrest backend returns one, the AssertJ backend two, the Kotlin DSL backend one Kotlin file, or none). Backends are discovered withjava.util.ServiceLoaderfrom the processor path, and every backend found runs for every configured type. Backends depend only on the core, never the other way round, with one exception: the Kotlin DSL backend depends on the Hamcrest backend. The Enforcer plugin and ArchUnit tests guard this.
5.2. Level 2
5.2.1. White Box matcher-generator-core
MatcherGenerationProcessor-
The annotation processor (package
io.github.marmer.matchergenerator.core) registered throughMETA-INF/services. It loads the backends and hands every round to the worker. It declares the processor optionkapt.kotlin.generated. MatcherGenerationProcessorWorker-
Resolves the configured class and package names to types, skips types that are already marked as generated, asks every backend for its source and writes it.
TypeModelFactory-
Reads a type with the compiler’s element API and creates the
TypeModel: getters for beans, components and accessors for records, inherited properties and public member types. NullabilityResolver-
Derives the
Nullabilityof a property for theTypeModel: primitives are non null, JSpecify and JetBrains@Nullable/@NonNull/@NotNullon the accessor decide explicitly, and otherwise the closest@NullMarkedor@NullUnmarkedscope does. A type variable is null-agnostic and stays unspecified. Kotlin nullness is read from the JetBrains annotations kotlinc emits, not fromkotlin.Metadata. The worker also sets theGenerationContextflag that tells backends whetherorg.jspecify.annotations.NullMarkedresolves, which is the condition for emitting JSpecify annotations. TypeModel,PropertyModel,Nullability-
The immutable model that backends work on (see the glossary term "Property model").
5.2.2. White Box hamcrest-matcher-generator
HamcrestGeneratorBackend-
The Hamcrest implementation of
GeneratorBackend, registered throughMETA-INF/services. For everyTypeModelit asksMatcherGeneratorfor the<Type>Matchersource and returns it as oneGeneratedSource. MatcherGenerator-
Builds the source of a
<Type>Matcherclass with JavaPoet from aTypeModel: theisX()entry point,withFoo,resetFooandignoringFoofor every property,checkingAllProperties()and theisXLike(reference)method. BeanPropertyMatcherand its helpers-
The runtime library that the generated matchers call at test time. It reads the checked properties of the actual object, collects the failing checks with their property paths and builds the mismatch message (see chapter 6). Its helpers, the collection matchers (
itemsInOrder,itemsInAnyOrder,entries),ReferenceMatching,PathWalkandMismatchReport, are described in the building block table above. The library extends Hamcrest’sTypeSafeMatcher.
6. Runtime View
6.1. Generation of matchers
The compiler runs the processor in every round in which it finds @GenerateMatchers or its 5.x alias @MatcherConfiguration. For every configured type
the following happens:
-
MatcherGenerationProcessorhands the round toMatcherGenerationProcessorWorker, which resolves the configured class and package names to types. Names that match nothing are a compiler error for@GenerateMatchersand a warning for the 5.x alias, which keeps 5.x configurations compiling. Theexcludeentries then leave out the configured types they name by class or package, and the public member types they name are not modelled as nested types. Anexcludeentry that matches neither a configured type nor one of their public member types is a warning. Types that carry the processor’s own@Generatedmarker are skipped with a note. -
TypeModelFactorycreates theTypeModelof the type, including its public nested types. A configured type without properties produces a compiler warning when the configuration names it; a type that only a package entry picks up gets a verbose note instead, since packages hold types that are no match target. Each skipped method is reported with its reason, but only when the processor optionmatchergenerator.verboseis given; without it the compiler prints no notes. -
The worker calls every
GeneratorBackendfound throughServiceLoaderwith that model and aGenerationContext(its content is listed in chapter 5, Important Interfaces). The worker models every configured type of the round first and generates afterwards, so a backend knows the types of other configurations too. There is no selection: the backends on the processor path are the user’s choice. -
Each backend returns its
GeneratedSourcefiles, one per generated class. The AssertJ backend also returns oneSoftAssertionsProvidermixin per target package, from the first type of that package by name (see the glossary entry Soft assertions mixin), and the same type writes thePropertyRepresentationof that package (see the glossary entry Generated representation). The worker writes Java sources through the compiler’sFiler, so they are compiled in the same build. -
Kotlin sources are written into the directory named by the processor option
kapt.kotlin.generated, which only KAPT provides (see ADR 0004). TheGenerationContexttells the backends whether that option is there. The Kotlin DSL backend generates nothing without it and reports one compiler note, once per compilation; a backend that returns Kotlin anyway makes the worker report one compile error that names the backend and the type, because the Kotlin file would otherwise never be compiled. A type without properties of its own gets no DSL file, although its Java matcher exists. The scope classes and entry functions of all types in a package are top level declarations of that package, so the backend names them from the models of all types generated in the run; the naming rule and the error for names that still collide are in chapter 5.
- Files written once per package
-
A backend may write a file that exists once per target package, like the AssertJ
SoftAssertionsProvidermixin andPropertyRepresentation. It does so from the first type of the package in the round, so two configuration classes of one package in one round still produce one file with the types of both. The generated API stays the same in every build: the name of the mixin does not depend on the configuration, so a test class that implements it keeps working when a type is added.What a round cannot see is the file of an earlier round or compilation (decision: ADR 0006). The rule is: a file is written once per compilation, and the first round that writes it decides its content. The worker lives once per compilation and remembers every source it wrote, so the rule below is generic: it applies to every
GeneratedSourceof every backend, not only to files written once per package.-
A later round of the same compilation, e.g. because another processor generated a configuration class, that produces a source whose name an earlier round wrote does not write it again (the
Filerwould refuse). The worker reports one warning at the annotation that configured the type, naming the file and the types of this round in its package that it does not know. Add those types to the configuration of the first round, or generate the configuration class in a round before the matcher generator runs, to have them in the file. -
A second compilation of the same package (main and test, KAPT) writes its own file, and the compiler prefers it for that compilation. When the AssertJ backend finds a
SoftAssertionsProviderof that package already on the class path outside the output directory of the running compilation, it reports a note naming the types the new mixin knows;PropertyRepresentationgets no such note. The class that a previous, non clean build of the same module left in its own output is replaced by this build and therefore no note. The mixin of the second compilation does not contain the types of the first. -
The Hamcrest
isXLike(reference)compares a property deeply only when the matcher for its type is generated in the same round. A property whose type is a class of the user without a generated matcher in the round is compared withequals; with-Amatchergenerator.verbosea note names the property and the type. Library types (java.,javax.,kotlin.), enums and types generated in the round, also those without properties, do not get a note.Optional<X>is ajava.type and is not unwrapped: it is compared withequalswithout a note.
-
A round without configuration can still reach the processor: the matchers written in an earlier round come back carrying javax.annotation.processing.Generated and org.jspecify.annotations.NullMarked. The processor claims these two annotation types in that round, and returns without generating anything. Without the claim, javac’s -Xlint:all -Werror builds would fail on annotations nobody claims. The side effect is that javac skips a later processor in that round when that processor supports exactly these annotation types and nothing else.
- Notable aspects
-
-
The model is built once per type and shared by all backends, so detection rules such as record components or getter naming exist only once.
-
A backend problem shows up as a compile error of the user’s build, not as a failure of the user’s tests. A backend reports it through
GenerationContext.reportError, once per problem, at the annotation (see chapter 8, Diagnostics).
-
6.2. Describing a mismatch (Hamcrest)
When an assertion with a generated Hamcrest matcher fails, the generated <Type>Matcher delegates describeTo and
describeMismatchSafely to its BeanPropertyMatcher, which builds the message in this order:
-
It reads every checked property of the actual object and collects the failing checks as
path: expected … but …lines. A check that holds a generated matcher (recognised throughGeneratedBeanMatcher) and meets an object of the matched type recurses into it with the extended property path (customer.address.city). If the object is missing (null) or of another type, the whole nested expectation is one failure. -
A check that holds any other matcher is a foreign check: the runtime asks that matcher whether it matches and for its description and mismatch. Around these calls a per-thread depth counter is raised.
-
A generated matcher that Hamcrest’s own matchers (
contains,hasItem,hasEntry) call while that counter is above zero renders on one line (OrderLine {sku "X-9", quantity <3>}); otherwise it renders as a block (Order with 4 property checks, one failing check per line). -
Values are printed by a
Descriptionthat shows values of generated types from their properties, never viatoString(). -
With the completeness check on (
checkingAllProperties()), the description ends in, every property checked or ignored, and after the failing checks the mismatch adds one line that lists the properties neither checked nor ignored, with their nested paths, asneither checked nor ignored: customer.phone, customer.address. The golden testBeanPropertyMatcherTestpins this line.- Notable aspects
-
-
The counter is raised only by generated matchers. A generated matcher that a foreign matcher holds directly, as in
assertThat(list, contains(isOrderLine())), keeps the block format, because nothing generated has called the foreign matcher. -
The properties printed for a type are the ones the first generated matcher of that type reported; properties every object has (
class) are left out.
-
6.3. Describing a mismatch (AssertJ)
The generated AbstractXAssert fails through AssertJ’s own failure, so the message keeps AssertJ’s form, with the path of
the failing property from the root type. A value that differs reads Expecting Order.customer.address.city to be:, the
expected value on its own line, then but was: and the actual value. A null nested object reads Expecting Order.customer
not to be null. The path is carried by AssertionPath into the nested assertion, see the glossary entry "Assertion path".
The completeness check runs after the value checks. When properties are neither addressed nor ignored, the assertion fails
with Expecting every property of Order.customer to be checked or ignored, but these were neither: phone, address. The
sentence names the path of the asserted object from the root type, so a nested check reads Order.customer, the path of the nested assertion, and not only the nested type. The test
CompletenessCheckTest of the AssertJ end-to-end module pins these messages.
6.4. Release
The maintainer merges the Release PR that release-please keeps up to date. The merge creates the tag and the release page, and the same workflow then builds the tag on GitHub Actions. Only a green build of the tag publishes the signed bundle to Maven Central, and the documentation is deployed to GitHub Pages from the same tag. Finally the workflow announces the release to the maintainer. The workflows, jobs and credentials are described in chapter 7.
7. Deployment View
7.1. Infrastructure Level 1
The project has no runtime infrastructure of its own: the processor runs inside the compiler of the user’s build. What is deployed are the build artifacts and this documentation.
- Motivation
-
Users take the artifacts from Maven Central, and readers take the documentation from GitHub Pages. Both are produced by automated builds, so no manual upload step exists.
- Quality and/or Performance Features
-
Every published jar is signed and comes with a sources jar and a Javadoc jar, which Maven Central requires. A CycloneDX software bill of materials is attached to every published module, so users can see which dependencies it brings along.
- Mapping of Building Blocks to Infrastructure
| Building block | Deployed to |
|---|---|
|
Maven Central through the Central Portal. |
|
Not published. The release bundle excludes them ( |
Documentation |
GitHub Pages, deployed by the |
7.2. Infrastructure Level 2
7.2.1. Release to Maven Central
The release Maven profile signs the jars with GPG, attaches the sources jar, the Javadoc jar and the CycloneDX SBOM,
and lets the central-publishing-maven-plugin upload one bundle to the Central Portal. The plugin publishes the
bundle automatically and waits until it is published. The former OSSRH is not used any more.
7.2.2. Documentation on GitHub Pages
The Docs workflow builds the docs module on JDK 21, uploads the rendered HTML as a Pages artifact and deploys it
with the official Pages actions. It runs for version tags and on manual request.
The site is published on its custom domain, the one base URL of the docs build (docs.base.url in docs/pom.xml).
Every page carries a canonical link to its address there. The site root also holds sitemap.xml (every rendered page),
robots.txt (every crawler allowed, names the sitemap) and llms.txt. The github.io address of the project only
redirects to the base URL.
7.3. CI pipeline
Pull requests and pushes to main run these workflows on GitHub-hosted runners:
-
.github/workflows/ci-build.yml(CI-Build) runs the jobsBuild(mvnw -B clean install -DskipTests),Verify JDK 17,Verify JDK 21,Verify JDK 25andVerify JDK latest(the matrix;latestfollows the newest GA JDK),Release dry run(the release profile withverify, thenscripts/ci/check-release-artifacts.sh),Mutation testing(PIT, with a history cache keyed on the PIT version) andSonar(validatesSONAR_TOKENwithscripts/ci/check-sonar-token.pybefore the build, then runs the scanner with the quality gate), andExample Gradle(validates the wrapper jar with the SHA-pinnedgradle/actions/wrapper-validation, runsmvnw -B -DskipEndtoend -DskipTests install, thengradlew buildinexample-gradle-javaon JDK 21 and again on JDK 17, because users copy the example on both; the example resolves the reactor’s artifacts frommavenLocal()).Example Gradlehas no Gradle cache (the build is small and a cache is state that can differ between runs) and a 30 minute time limit; the wrapper distribution is pinned bydistributionSha256Sum. -
.github/workflows/domain-docs-sync.yml(Domain docs in sync) runs the domain docs check, the frozen 5.x suite check,scripts/ci/check-project-version.py(every module pom’s own and parent version equals the manifest version, is anextra-filesentry, and project dependencies have no literal version),scripts/ci/check-workflow-hardening.py, the unit tests ofscripts/ciandscripts/agents, and the check of the ruleset’s job names. It also runsscripts/ci/sync-readme-snippets.py --check, which fails when a README snippet no longer matches its tagged example region; the README’sx-release-please-versionlines are raised by release-please through theREADME.mdentry ofextra-files. -
.github/workflows/dependency-review.yml(Dependency review) runs on pull requests,.github/workflows/scorecard.yml(Scorecard) weekly and on pushes tomain, and.github/workflows/scanoss.yml(SCANOSS) on pull requests and weekly. SCANOSS is non-blocking. -
.github/workflows/credential-reminder.yml(Credential reminder) runs weekly and on manual request, in a job of thereleaseenvironment (so it readsSONAR_TOKEN,CENTRAL_USERNAME,CENTRAL_PASSWORDandMAVEN_GPG_KEYthere). It validates the Sonar and Central Portal tokens (scripts/ci/check-sonar-token.py,scripts/ci/check-central-token.py), thenscripts/ci/check-credential-expiry.pyreads the expiry of the GPG key (the Sonar token has no expiry date, so it raises no Sonar reminder) and the expiry dates (YYYY-MM-DD, set by the maintainer after each renewal) in the repository variablesCENTRAL_TOKEN_EXPIRES_ONandRELEASE_APP_KEY_EXPIRES_ON, and opens or updates one renewal issue for a credential that expires within 30 days.
The main ruleset requires Build, Verify JDK 17, Verify JDK 21, Verify JDK 25, Verify JDK latest, Domain docs in sync, Release dry run, Sonar, Mutation testing and Example Gradle. scripts/ci/check-ruleset-job-names.py checks that every required name is a job name, so renaming the Example Gradle job without changing the ruleset fails it. Dependency review, Scorecard and SCANOSS are not required. Scorecard reports only; its score does not gate merges; accepted findings are listed in chapter 11.
7.4. Release pipeline
Releases run on CI and need no machine of the owner.
-
.github/workflows/pr-title.yml(required checkPR title) enforces Conventional Commits on pull request titles, because the squash-merged title is the commit that release-please reads. -
.github/workflows/release.ymlruns on every push tomain. release-please (typesimple, default bump rules, configured inrelease-please-config.jsonand.release-please-manifest.json) keeps a Release PR up to date, using an App token so that the PR runs its checks and the new tag triggers the docs deployment. Tags have novprefix (6.0.0), matching the[0-9]*trigger ofdocs.yml. The project version lives in the module poms (own and parent<version>), which release-please raises through one xml entry per pom inextra-files, and in the Gradle example (markerx-release-please-version). Thesimpletype is used instead ofmavenbecause the Maven updater rewrites the@project.version@of the frozen 5.x suite and misses the parent versions. Thesimpletype’s optionalversion.txtis not created: its updater hascreateIfMissing: falseand the release-please 17.11.2 simulation of the 6.0.0 release changed onlyCHANGELOG.md, the Gradle example and the 20 poms. Dependencies on project artifacts use${project.version}, the 5.x suite and its runner’s consumer poms are filtered by the build (@project.version@).scripts/ci/check-project-version.pyguards all of it. -
Merging the Release PR creates the tag and the release page. In the same workflow the job
Release buildrunsmvnw verifyon the tag, and only if it is green the jobPublish to Maven Centralrunsmvnw -P release deploy(GPG signatures, SBOM,central-publishing-maven-pluginwaiting forpublished), attaches a build-provenance attestation (actions/attest) and makes sure the release page exists. -
After a confirmed publish, the job
Announce releasecomments on the merged Release PR with the App token, mentioning the owner and linking Maven Central, the release page and the CHANGELOG section. -
A failed publish is re-run with
workflow_dispatchand the existing tag as input; tags are never moved. -
6.0.0 is forced with
release-asand merged by the owner. Auto-merge of Release PRs stays off until after 6.0.0.
The credentials live in the repository environment release (CENTRAL_USERNAME, CENTRAL_PASSWORD, MAVEN_GPG_KEY, MAVEN_GPG_PASSPHRASE) and in the App settings (RELEASE_APP_ID, RELEASE_APP_PRIVATE_KEY).
8. Cross-cutting Concepts
8.1. Continuous integration gates
Every push runs a small set of automated gates. Each one exists because a defect once reached the main branch or
blocked it. The checks the main branch ruleset requires before GitHub merges a pull request are listed in chapter 7.
| Gate | What it guarantees |
|---|---|
Build and Verify matrix ( |
The reactor installs without tests, and |
JaCoCo floor (part of |
Each production bundle ( |
Release dry run |
The |
Mutation testing ( |
PIT mutates the production code of the published modules, and each module must detect at least 90 % of its mutants
( |
Sonar quality gate ( |
SonarCloud analyses the build and its quality gate fails the job when new code adds issues, duplicated or uncovered code. Old code is not judged again when a rule changes. |
Example Gradle ( |
The Gradle example outside the reactor builds against the artifacts of the reactor, so the guide’s Gradle setup keeps working on JDK 21 and on JDK 17 (see chapter 7). |
Dependency review ( |
A pull request that adds a dependency with a known vulnerability is blocked. |
License gate (part of the Build and verify matrix) |
Every project dependency that a published jar needs uses a permissive license (Apache-2.0, MIT, BSD-2-Clause or
BSD-3-Clause); test-scope dependencies may also use EPL-1.0 or EPL-2.0. The |
Static analysis (JDK 21+ profile |
Spotless checks google-java-format (AOSP style, 4-space indentation) and the SPDX header in every Java and Kotlin source
of the modules, including tests and end-to-end sources, except the frozen 5.x suite, which stays frozen and so carries no SPDX headers (deliberate deviation from #128). Error Prone runs its default checks on every module at ERROR
with no baseline. NullAway runs at ERROR in JSpecify mode on the |
The |
|
Ruleset against job names (part of |
Every status check that the |
Docs sync ( |
The agent-facing glossary and ADRs are mirrored in these chapters, the required checks named in the agent docs are
CI job names, and the file names and packages that |
8.2. Diagnostics
The core reports a misconfiguration as a compiler diagnostic at the annotation that configured the type, once per
problem and naming the offending value (the unresolvable names of #113). A backend that finds a problem in a configured
type reports it the same way through GenerationContext.reportError(message): the core prints the message as a compiler
error at the @GenerateMatchers annotation. The backend leaves the offending member out of its generated code, so the
error is the only one the user sees. The AssertJ backend does this for a generated method whose name an inherited
AssertJ method already has: one error per property, naming the property and the clashing methods. It also reports two
properties that generate the same method name (such as a boolean foo and notFoo, both isNotFoo()) and two nested
types whose factory constants share a name (such as FooBar and Foo_Bar, both FOO_BAR), one error per name. It
also reports a top level class name that two types of one package need (Foo and AbstractFoo both generate
AbstractFooAssert, or a type named like the package’s mixin SoftAssertionsProvider) and then writes nothing for
the package, while FooBar, Foo_Bar and FOOBAR are different classes and no clash; like the other per-package
files it sees only the types of one processing round (ADR 0006).
A backend that leaves something out of its generated code without failing the build reports it through
GenerationContext.reportWarning(message): the core prints the message as a compiler warning at the same annotation. The
Kotlin DSL backend uses it for a property whose name clashes with a member of its DSL scope; that property is left out of
the DSL and the Java matcher keeps it.
9. Architecture Decisions
9.1. ADR 0001: Shared property model with separate library backends
Status: accepted (6.0.0 planning)
Version 6.0.0 generates assertions for both Hamcrest and AssertJ from a single configuration. To keep the two libraries cleanly apart, a library-independent core module builds a property model of each configured type, and each assertion library has its own backend module that turns this model into code. Backends depend only on the core and are discovered via Java’s ServiceLoader, so users pick a library simply by adding its artifact to the annotation processor path. The dependency rules are enforced by Maven Enforcer and ArchUnit. The existing @MatcherConfiguration annotation keeps working unchanged as a deprecated alias, and the migration is carried out behind the existing automated tests.
Details: ADR 0001
9.2. ADR 0002: Coordinates, packages and 5.x compatibility
Status: accepted (6.0.0 planning)
The project keeps its name and the group ID io.github.marmer.testutils. Core and annotations get neutral matcher-generator- artifacts, the Hamcrest artifact keeps its name, and AssertJ gets assertj-assertion-generator. New code lives in io.github.marmer.matchergenerator., and the new annotation is @GenerateMatchers. Only the deprecated @MatcherConfiguration stays in the old package. The two 5.x artifacts stay as code-free bridge jars that depend on the Hamcrest artifact, because older compiler plugins do not follow a relocation in the processor path (amendment of 2026-10-27). 5.x user tests should compile after a version bump alone. The Central namespace io.github.marmer is verified in the Central Portal, so these coordinates stay usable.
Details: ADR 0002
9.3. ADR 0003: Docs as code with a Structurizr C4 model and embedded examples
Status: accepted (6.0.0 planning)
Outdated snippets, broken links and missing images are common in project documentation. To rule them out, the whole documentation is built by the normal Maven build. A dedicated docs module renders the user guide and this architecture documentation with Asciidoctor. Code shown in the guide is included directly from example Maven modules, which are compiled and tested in the same build. The diagrams of the context, building block, runtime and deployment views are C4 views generated from a single Structurizr model, so the levels stay consistent and need little explanatory text. The build fails on Asciidoctor warnings, and tests check links, anchors, images and that every modelled view appears in the documentation. Because the current Structurizr libraries require Java 21, the documentation is only built on JDK 21 or newer.
Details: ADR 0003
9.4. ADR 0004: Kotlin DSL as a Kotlin-emitting backend on top of the Hamcrest backend
Status: accepted (6.0.0 planning)
Kotlin users get an optional, type-safe DSL such as isOrder { customer { name("Ada") } }, which builds on the generated Hamcrest matchers. Because it needs one wrapper per configured type, it is generated as well, by a separate opt-in artifact hamcrest-matcher-generator-kotlin. This artifact is another backend behind the same plug-in interface as Hamcrest and AssertJ; that interface therefore returns Java or Kotlin source files, one or more per type. It is the only backend allowed to depend on another backend, the Hamcrest one, whose naming it reuses, and the build enforces that exception explicitly. The generator itself is written in Java and produces Kotlin with the KotlinPoet library, so the project’s production code stays plain Java. It runs only under KAPT, the Kotlin compiler’s way of running annotation processors; under plain javac it skips the DSL and prints one note.
Details: ADR 0004
9.5. ADR 0005: Java 17 baseline for 6.0.0
Status: accepted (6.0.0 planning)
Version 6.0.0 needs Java 17 or newer, for the annotation processor, the runtime artifacts and the generated code. Earlier planning assumed Java 11, but the libraries and tools this project updates automatically have moved on: JUnit 6 and AssertJ 4 need Java 17, Maven 4 needs it to run, and some tools already need Java 21. With Java 11, every such update would stop and wait for the maintainer, which contradicts the goal of a project that maintains itself. A major release is the right moment for this break. Users who stay on Java 11 keep using 5.x; everyone on Java 17 or newer upgrades by changing the version number.
Details: ADR 0005
9.6. ADR 0006: Files written once per package, and the backend SPI channels that report it
Status: accepted (2026-10-29)
Some generated files exist once per target package, such as the AssertJ soft assertions mixin, and they only know the types of the processing round that wrote them. Instead of failing with a Filer exception or leaving types out silently, the file is written once per package, the first round decides its content, and a later round or a second compilation (main and test, KAPT) gets a diagnostic. Naming the mixin per configuration was rejected: a test class that implements it would break when the configuration layout changes, and the types of earlier rounds would still be missing. To report this, the backend plug-in interface gained a verbose-only note and a question whether a class exists outside the compilation’s own output; the rule that a source is not written again applies to every backend. Consequences are that builds with warnings as errors fail on the late-round warning and that the class path note cannot tell every case apart. A diagnostics interface instead of the growing list of channels is recorded as an open idea.
Details: ADR 0006
10. Quality Requirements
The quality requirements refine the five quality goals from chapter 1.2 into concrete, measurable scenarios. They are classified by the quality model of ISO/IEC 25010:2023. Characteristics that are not a quality goal of their own were checked for gaps; where they matter for a build-time test library, they got scenarios as well.
10.1. Quality Requirements Overview
The table maps every quality goal and every further relevant ISO/IEC 25010:2023 characteristic to its scenarios in chapter 10.2.
| Quality goal | ISO/IEC 25010:2023 characteristic / sub-characteristic | Scenarios |
|---|---|---|
1 Correctness |
Functional suitability / Functional correctness; Reliability / Faultlessness |
|
2 Non-intrusiveness |
Compatibility / Co-existence |
|
3 Usability |
Interaction capability / User error protection, Self-descriptiveness, Learnability, Operability |
|
4 Build-tool agnosticism |
Compatibility / Interoperability; Flexibility / Adaptability |
|
5 Maintainability |
Maintainability / Modifiability, Modularity, Analysability |
|
No quality goal |
Compatibility / Co-existence; Flexibility / Replaceability; Security / Integrity, Authenticity; Reliability / Fault tolerance; Performance efficiency / Time behaviour |
The remaining characteristics have no scenarios, for these reasons:
-
Safety: the library has no physical effect on people or the environment.
-
Reliability / Availability and Recoverability: the library is not a service; it only runs while a project compiles and tests.
-
Security / Confidentiality, Non-repudiation and Accountability: the library processes no user data and has no users of its own; integrity and authenticity of the released artifacts are covered by QS-X3.
-
Interaction capability / Inclusivity and User engagement: the library has no user interface; the project site’s accessibility is not a quality of the architecture.
-
Performance efficiency / Resource utilization and Capacity, Flexibility / Scalability: the only load is the number of configured types at compile time, which QS-X5 covers.
-
Flexibility / Installability: adding the library is a plain build dependency; QS-U3 and QS-B1 cover it.
10.2. Quality Scenarios
Each scenario is one sentence that names all elements of the SEI scenario form (source, stimulus, environment, artifact, response and response measure), following this pattern:
When source stimulus while environment, artifact response; response measure.
Scenarios describe either a use of the library at runtime (a test developer compiles or runs tests) or a change to the system or its environment (a contributor, a bot or a new JDK). The column "Protected by" names the check that decides whether the scenario is met, as decided for 6.0.0. "None yet (planned for 6.0.0)" marks a scenario without such a check; 6.0.0 adds that check.
| ID | Characteristic | Scenario | Protected by |
|---|---|---|---|
Functional suitability / Functional correctness |
Wrong property is caught. When a test developer asserts an object that differs from the expected one in exactly one property while running a JUnit test on any supported JDK, the generated Hamcrest matcher or AssertJ assertion fails that test; this holds in both backends for every property kind of the sample set (primitive, wrapper, |
Annotation processor integration tests; end-to-end modules on every JDK |
|
Functional suitability / Functional correctness |
A new property is not silently ignored. When a developer adds a property to a model class without touching a test that opted into the completeness check ( |
End-to-end modules |
|
Reliability / Faultlessness |
A faulty change is stopped. When a contributor opens a pull request that lets a generated assertion pass for a wrong object while CI runs the required checks, the build of the affected processor, runtime or backend module turns red and the pull request cannot be merged; a mutation score of at least 90 % per module and at least 99 % line and branch coverage of production code keep such a change from slipping through. |
Build; Mutation testing (PIT); JaCoCo coverage floor |
|
Functional suitability / Functional correctness |
A cyclic object graph ends the comparison. When a test developer compares an object whose graph contains a cycle (such as |
Runtime unit test |
|
Compatibility / Co-existence |
Production code stays untouched. When a project adds the generator and |
|
|
Compatibility / Co-existence |
A small footprint on Java 17. When a test developer runs tests on Java 17 while the generated code and its runtime are on the test classpath, they run with no dependencies beyond Hamcrest or AssertJ, JSpecify and the project’s own runtime artifact; bytecode level 17 and this dependency list are verified on every build. |
Maven Enforcer; Verify JDK 17 |
|
Compatibility / Co-existence |
Generated code compiles without warnings. When a test developer compiles a project’s tests with |
End-to-end modules compiled with |
|
Interaction capability / User error protection |
A misconfiguration is reported where it happens. When a test developer configures a name that cannot be resolved, or a type whose generated names clash (method names with inherited ones, AssertJ top level class names in one package, Kotlin DSL scope names), while compiling with javac or KAPT, the annotation processor reports one compiler error per problem at the annotation element and names the offending value; there is no stack trace, and a backend writes nothing for a clashing name, so no follow-up error appears in generated code. |
Annotation processor integration tests |
|
Interaction capability / Self-descriptiveness |
A failure explains itself. When a test developer’s assertion with a generated Hamcrest matcher fails while the tests run in the IDE or on CI, the matcher’s mismatch message lists every failing check on its own line with property path, expected value and actual value (for example |
Annotation processor integration tests and runtime tests with golden messages |
|
Interaction capability / Learnability |
The first green assertion comes from the README. When a developer new to the library copies the README snippet into a Maven or Gradle project on JDK 17 or later, the library generates an assertion that turns the developer’s first test green; this takes at most 10 minutes, and the snippet always compiles because CI keeps it in sync with an example module. |
Example modules; README snippet sync check |
|
Interaction capability / Operability |
Kotlin properties feel native. When a Kotlin developer configures a class with properties such as |
Kotlin end-to-end modules |
|
Compatibility / Interoperability |
The same API with every build tool. When a test developer applies the same configuration with Maven (classpath discovery or |
Build; Verify JDK 17/21/25/latest |
|
Flexibility / Adaptability |
A new JDK shows up early. When a new JDK version reaches general availability while the project runs on autopilot, Renovate moves the CI matrix’s |
Verify JDK latest (Renovate-managed) |
|
Maintainability / Modifiability |
A green major update needs no owner. When Renovate opens a major update of a dependency (such as Hamcrest, AssertJ, JUnit or a Maven plugin) while all required checks pass, the pull request is merged and released automatically; the owner spends no time on it. |
Required checks; auto-merge; release-please |
|
Maintainability / Modifiability |
A new property kind stays local. When a contributor adds support for a new property kind (such as |
Maven Enforcer; ArchUnit tests |
|
Maintainability / Modularity |
A new backend plugs in. When a contributor adds a third backend (such as one for Google Truth) to the main branch, it is a new module behind the backend SPI; no line changes in the core or in the existing backends. |
ArchUnit tests; ADR 0001 |
|
Maintainability / Analysability |
Code quality does not erode. When a contributor’s pull request introduces new issues, duplicated code or uncovered code while CI analyses it, the Sonar quality gate fails and blocks the merge; new code adds no issue. |
Sonar |
|
Compatibility / Co-existence |
The generator works alongside other processors. When a test developer runs the generator in the same compilation as Lombok and other annotation processors, the annotation processor sees the accessors Lombok generates and leaves the other processors' output unchanged; the generated matchers cover every Lombok-generated getter, and the build reports no processor error. |
|
|
Flexibility / Replaceability |
Upgrading from 5.x is a version bump. When a 5.x user raises the version to 6.0.0 in an existing project that builds on JDK 17 or later, the user’s tests compile and pass against the newly generated matchers; no line of test code changes (the bridge jars under the old coordinates cover the changed artifact, also for compiler plugins older than 3.12, which do not follow a relocation in the processor path). |
The frozen 5.x suite, built as a consumer build against the reactor’s bridge jars (see chapter 8, Frozen 5.x suite); |
|
Security / Integrity, Authenticity |
Releases are verifiable. When a user or their supply-chain tooling downloads a release from Maven Central, the artifacts' GPG signatures, the GitHub build-provenance attestation ( |
Release workflow ( |
|
Reliability / Fault tolerance |
Unusual input never crashes the compiler. When a test developer configures types with generics, inner classes, no properties or cyclic references between types while compiling with javac or KAPT, the annotation processor generates matchers and warns about each configured type without properties; the compilation never ends with an uncaught exception from the processor. |
Annotation processor integration tests |
|
Performance efficiency / Time behaviour |
Generation stays fast. When a test developer configures 200 types while compiling on a GitHub-hosted CI runner, the annotation processor adds at most 2 seconds to the compilation. |
None: accepted risk, see chapter 11 |
11. Risks and Technical Debts
After 6.0.0 the project is meant to run on autopilot: bots update dependencies, merge green pull requests and publish releases, and the maintainer only steps in when something turns red. The risks below are the ways this can stall or go wrong; the technical debts are deliberate compromises the architecture carries.
Each entry is rated on two scales:
-
Probability that the risk occurs, or that the debt causes harm: low (1), medium (2) or high (3).
-
Impact if it does: tolerable (1, a nuisance that does not stop development or users), disabling (2, merges, releases or a feature stop until someone acts; recoverable) or catastrophic (3, harm that cannot be undone, such as a malicious release signed with the project’s key).
The list is ordered by priority, the product of both values, highest first; entries with the same product are ordered by urgency.
| Risk or technical debt | Probability | Impact | Description | Measures |
|---|---|---|---|---|
Risk: credentials expire |
medium |
disabling |
The autopilot depends on four credentials: the SonarCloud token (a personal token with no fixed validity period, which SonarCloud removes after 60 days without use), the GPG signing key (three years), the Maven Central Portal token (from the legacy username/password account) and the private key of the GitHub App that creates release pull requests. When one expires or is revoked, either every merge stops (Sonar is a required check) or releases fail. |
A weekly reminder workflow ( |
Risk: free external services change or fail |
medium |
disabling |
The project relies on services that are free for open-source projects: GitHub Actions and Pages, SonarCloud, the Mend Renovate app, the Maven Central Portal and release-please. Any of them may change its terms, rate-limit (the free Renovate app already runs only every few hours) or be unavailable. Because Sonar is a required check, a SonarCloud outage blocks every merge. |
Accepted. All required checks are the project’s own workflow jobs, not checks posted by the vendors, so a failing service shows up as a clearly named red job. During an outage the maintainer may merge past the ruleset. Fallbacks are known: Renovate can run self-hosted in GitHub Actions, and the Sonar job can be made non-blocking. |
Risk: JDK internals break the newest JDK leg |
medium |
disabling |
The annotation processor’s integration tests compile code in memory with Google Compile Testing, which needs |
Renovate moves the |
Risk: a single maintainer |
medium |
disabling |
Every red pull request, credential renewal and baseline decision lands on one person. If the maintainer is unavailable, updates stop and users receive no fixes. Nothing broken gets merged, because red pull requests wait. |
Accepted. The autopilot keeps routine work away from the maintainer. A maintainer runbook ( |
Risk: the Java baseline erodes again |
medium |
disabling |
6.0.0 requires Java 17 (ADR 0005). Libraries and tools keep raising their own minimum; Structurizr and Error Prone already need Java 21. When a dependency on the users' classpath or a build tool needed on the JDK 17 leg requires a newer Java version, its update cannot be merged automatically. |
The case shows up as one red Renovate pull request (Maven Enforcer’s bytecode check or the JDK 17 leg). The maintainer either caps that dependency in the Renovate configuration with a comment naming the reason, or raises the baseline in a new major version. Tools that are only needed for analysis or documentation run in the JDK 21+ profile and do not affect the baseline. |
Risk: a compromised dependency is released automatically |
low |
catastrophic |
Green pull requests are merged without a human, and releases are published without one. A malicious version of a dependency or a GitHub Action that passes every check would be released to Maven Central, signed with the project’s GPG key, and reach every user who updates. |
Renovate waits until a version is at least three days old ( |
Technical debt: the 5.x compatibility layer stays forever |
high |
tolerable |
To let 5.x users upgrade by a version bump, 6.0.0 keeps the deprecated |
Accepted. The frozen 5.x suite (consumer build |
Technical debt: the Kotlin DSL backend depends on the Hamcrest backend |
high |
tolerable |
The architecture keeps backends independent of each other: each one depends only on the core. The Kotlin DSL backend is a deliberate exception. It builds on the Hamcrest backend and reuses its naming of the generated matchers ( |
The exception is allowed for exactly this one dependency, and Maven Enforcer and an ArchUnit test reject any other dependency between backends. Golden-file tests of the generated Kotlin code and the Kotlin end-to-end modules fail as soon as the Hamcrest naming changes without the DSL following it. |
Risk: a release builds with a wrong module version |
low |
disabling |
The release type |
|
Risk: the Kotlin DSL needs KAPT |
low |
disabling |
The Kotlin DSL is only generated when the annotation processor runs under KAPT, the Kotlin compiler’s way of running Java annotation processors. JetBrains puts its new development into KSP, which is out of scope for this project. If a future Kotlin release dropped KAPT, the DSL could no longer be generated. |
The DSL is optional: the generated Java matchers work from Kotlin without it, and they do not depend on KAPT. Renovate keeps the tested Kotlin version current, so a removal of KAPT shows up as a failing update instead of a surprise for users. |
Risk: strict quality gates stop the autopilot |
medium |
tolerable |
Merges require at least 99 % line and branch coverage. The CI jobs |
PIT, JaCoCo and the Sonar scanner are updated in their own Renovate pull requests, so the cause of a red build is unambiguous. The Sonar quality gate judges new code only, so new rules do not fail old code. Thresholds are never lowered silently; a change goes to the maintainer. |
Risk: a major update of a runtime dependency ships as a patch release |
low |
tolerable |
Renovate merges major updates of the libraries on the users' classpath (Hamcrest, AssertJ, JSpecify) automatically, as |
Accepted. The end-to-end modules exercise the generated code against the new version before it is released, Maven Enforcer rejects a dependency that needs a newer Java version, and the release notes generated by release-please name the dependency update. Users can pin the library version in their own build. |
Risk: the generator’s run time is not measured |
low |
tolerable |
Scenario QS-X5 asks that generating assertions for 200 configured types adds at most 2 seconds to a compilation. No check measures this, so a change that slows down the annotation processor would go unnoticed until users complain about slow builds. |
The risk is accepted: projects configure far fewer types than that, and a run-time check on shared CI runners would produce unreliable results. Should users report slow builds, a benchmark module can be added later. |
11.1. Accepted OpenSSF Scorecard findings
The OpenSSF Scorecard (.github/workflows/scorecard.yml) reports the repository’s posture; its score does not gate merges. The table below lists every Scorecard check that was accepted instead of fixed, with the reason and the owner’s decision. It is separate from the table above, because a Scorecard check measures a practice and not a risk that occurs, so the probability and impact scales do not apply. The entries rest on the Scorecard API result of 2026-10-31 (analysed commit 95d1727, Scorecard v5.6.0, total score 7.7) and on the ruleset as the owner left it. The date is the owner’s decision where one is recorded, otherwise the date of that result.
Fuzzing (score 0 on 2026-10-31) has no entry: the project has a Jazzer fuzz test (ItemsInAnyOrderFuzzTest), and an entry is written only if a weekly Scorecard run after it still reports 0.
CII-Best-Practices (score 0) has no entry yet: the badge registration (issue #284) is still open. It gets an entry only after the owner has decided the unmet criteria of the passing level.
| Check | Score | Reason | Owner decision | Date |
|---|---|---|---|---|
|
0 |
Scorecard counts 0 of 29 changesets as approved by a reviewer. The project has one maintainer, and pull requests the maintainer merges are never counted as reviewed under any setup. An automatic bot approval was rejected, because it would show a review practice that does not exist. |
Accepted (#285). The |
2026-11-01 |
|
3 |
Scorecard warns that |
Accepted (#285). The ruleset is unchanged. |
2026-11-01 |
|
9 |
The Gradle wrapper jar of the Gradle example, |
Accepted (spec #104 plan). |
2026-10-31 (Scorecard result) |
|
10 |
Warning: |
Accepted (spec #104 plan). |
2026-10-31 (Scorecard result) |
|
6 |
Scorecard counts the contributing organizations: 2 companies or organizations. The project cannot influence this. |
Accepted (spec #104 plan). |
2026-10-31 (Scorecard result) |
12. Glossary
| Term | Definition |
|---|---|
Property model |
The description of a configured type and its properties that does not depend on any assertion library. All backends share it. In code it consists of |
Nullability |
Whether a property may be |
Property |
A readable value of a bean (via its getter) or a record (via its component accessor) that generated assertions can check. Every generated matcher also has |
Backend |
A generator that turns the property model into assertion code for one specific assertion library, such as Hamcrest or AssertJ. |
Assertion class |
The pair of classes the AssertJ backend generates per configured type: |
Soft assertions mixin |
The interface the AssertJ backend generates per target package, named |
Generated representation |
The class the AssertJ backend generates per target package, named |
Bridge jar |
The code-free jar that a 5.x artifact ( |
Configuration |
The single annotation-based declaration of the types for which assertions are generated. It is the same for every backend. |
Reference object |
An instance of a configured type whose property values are used as the expected values for all of its properties, so that a whole object can be compared in one step with |
Property path |
The route from the asserted object to the value that was checked, written with dots between property names, |
Assertion path |
The AssertJ counterpart of the property path: the path of an AssertJ failure, rooted at the asserted type ( |
Generated bean matcher |
A generated Hamcrest |
Collection matcher |
A runtime matcher for a collection or map property, created by |
One-line rendering |
The form in which a generated matcher describes itself and its mismatch, |
Completeness check |
The check that every property of a type is either checked or consciously ignored. It makes sure that a property added later cannot go unchecked without anyone noticing. Opt-in per assertion. A property counts as addressed by a check ( |
DSL scope |
The Kotlin receiver block for one configured type ( |
Generated code |
The source files that the processor writes into the user’s project at compile time: matchers, assertions and Kotlin DSL files. They belong to the user and are not covered by the project’s license. |
Runtime library |
The published artifact that generated code calls while the user’s tests run, such as the Hamcrest |