Public API in Groovy is covenanted: applications and libraries depend on it across releases, and removing or breaking it has costs we don't always see locally. This document is the contributor-facing reference for what is public, what is internal, what counts as a breaking change, and how the build helps us notice when one slips through.
If you're orienting in the codebase generally, start with
ARCHITECTURE.md. For build and submission
mechanics, see CONTRIBUTING.md. The user-facing
version-numbering scheme (SemVer since 2.0.0) is at
src/spec/doc/version-scheme.adoc.
Stability is signalled by package, by annotation, and occasionally by documented convention. The four tiers from most to least stable:
| Tier | How it's marked | Stability commitment |
|---|---|---|
| Public API | Lives in groovy.*, or in org.apache.groovy.* / org.codehaus.groovy.* without an internal marker |
Source- and binary-compatible across minor and patch releases. Breaking changes need a major version. |
| Incubating | Annotated @org.apache.groovy.lang.annotation.Incubating, or noted as "incubating" in the feature's prose documentation |
Reduced stability guarantee — design may still settle, so users opting in accept some risk of change in a minor release. See "Incubating features" below for what this means in practice. |
| Internal | Annotated @groovy.transform.Internal, or in a package whose name contains internal |
No stability guarantee. Treat as implementation detail, even if technically reachable. |
| Generated | Anything emitted by the build into build/generated/... or repackaged via the groovyjarjar* namespace |
Not API at all; avoid referring to these from anywhere stable. |
org.codehaus.groovy.* is a historical complication: most of it is
internal-by-intent but treated as public-by-practice because users
have come to depend on it. The safe assumption is that it's public
unless explicitly marked @Internal or living in an internal
sub-package.
The internal package convention is the one the build actually
enforces — see "The binary-compatibility check" below. Existing
examples include org.apache.groovy.internal.util.*,
org.apache.groovy.internal.metaclass.*, and
org.apache.groovy.parser.antlr4.internal.*.
Some internals are not Public API, yet are still part of the compatibility
contract because bytecode the compiler emits links against them directly
(call sites, ScriptBytecodeAdapter, indy bootstraps, transform helpers).
That surface is marked with
@org.apache.groovy.lang.annotation.GroovyABI.
- Who is protected: only the compiler↔runtime link, not user code.
@GroovyABIdoes not make something public API; it records that compiled Groovy may outlive the compiler version that produced it. since: mandatory, full three-part version form ("1.0.0", not"1.0"), giving the first release in which the element joined the ABI.- Placement: prefer the class level when every reachable member shares
the same
since; annotate members when theirsincediffers or only part of the type is reachable. Private fields and methods are never reachable by compiler-emitted bytecode and must not be annotated. - Retention:
CLASS— kept in published jars for build-time binary- compatibility checks. - Scope guidance: the marked set must be complete for a stated scope (e.g. the core always-emitted bridges) or explicitly work-in-progress, so it stays a trustworthy signal. Don't extend it to a whole GDK-style class unless those members are actually referenced by emitted bytecode.
Removing or changing an @GroovyABI-marked element is a breaking change to
compiled Groovy code, handled like any other break via the release-management
process below.
The complete annotated surface is recorded in
compatibility/groovy-abi-surface.json,
one entry per annotated element (method / field / constructor), keyed by JVM
descriptor — the exact signature emitted bytecode binds to. The file's schema
is compatibility/groovy-abi-surface.schema.json.
- Modules: the registry is a list of modules, each owning its elements by
nesting. Module ownership is explicit (never derived by package prefix), so a
whole-module removal (e.g. dropping
groovy-callsite) is visible as that module block disappearing rather than as scattered per-element breaks. - Allow-list: an
@GroovyABImay appear only in a tracked module (listed in the registry) or in an explicitly ignored module.checkGroovyABISyncfails if an annotation shows up anywhere else. sinceis the historical version that introduced each element, recorded alongside it; for class-level annotations every covered public method inherits the classsinceunless it carries its own.
checkGroovyABISync(part ofcheck): scans the compiled classes with ASM and fails if they disagree with the committed registry — an element removed, its descriptor changed, its annotation dropped (while the method still exists), or asincedecreased without authorisation.checkGroovyABIAgainstBaseline(PR mode): compares the committed registry against a baseline registry, e.g. from the merge-base (git show <base>:compatibility/groovy-abi-surface.json), passed via-PgroovyAbiBaselineRegistry=<file or JSON>.checkGroovyABIAgainstPreviousRelease(release mode): the same check against the previousX.Y.Zrelease tag's registry. When no previous registry exists (a release first creating the file, or a baseline before the file was introduced) the prior is treated as empty: only additions are expected, and the inaugural file must not contain tombstones.
The registry is also how intentional breaks are expressed and reviewed. An
element that is deliberately removed, loses its annotation, or has its since
changed is not deleted from the file — it stays as a tombstone record
marking where it used to be and why:
"tombstone": {
"type": "removed", // removed | annotation-removed | version-changed
"version": "6.1.0", // the release the break lands in
"previous-since": "2.0.0", // only for version-changed
"note": "replaced by ..."
}An unmarked removal / annotation drop / since change fails the check; a
tombstone authorizes it. The PR that breaks compatibility is the PR that
edits the registry, so the diff is the review surface.
Hard rule: a removed or annotation-removed tombstone requires
deprecated-since — the element must have been marked @Deprecated in the
previous major version line (for 6.x, the latest 5.y) before the break is
accepted. version-changed tombstones must record previous-since. This is
enforced mechanically once both sides have the file; at the 5.y→6.0 transition
the evidence has to come from the 5.y source (@Deprecated present there) and
human review, since 6.0 is the inaugural file.
@Incubating reduces the formal stability guarantee — it tells users
this surface may still change as the design settles — but project
practice is more conservative than the annotation's Javadoc wording
suggests. We don't break incubating APIs gratuitously: when a feature
has settled into the shape it wants and nothing else forces a change,
it stays as-is. @Incubating is the permission slip we use only when
the design genuinely has open questions, not a licence for churn.
Some features can't carry the annotation because they aren't expressed
as a class or method — a grammar tweak, a few lines added to an
existing visitor, a behavioural adjustment scattered across the
runtime. In those cases the incubating signal moves to the prose: the
feature's section under src/spec/doc/ (or the relevant subproject's
src/spec/doc/) says explicitly that the feature is incubating.
The reduced guarantee and the project's posture of caution before
changing them are the same as for an @Incubating annotation.
A new feature that should be incubating but can't carry the annotation gets the prose marker in the documentation in the same PR as the code change. When the feature graduates, the marker comes out of both places (annotation and prose) at the same time.
For anything in the Public API tier, all of the following are breaking:
- Removing a class, interface, method, field, or annotation type.
- Changing a method's name, return type, parameter list, or thrown checked exceptions.
- Narrowing visibility (public → protected, protected → package-private).
- Adding a method to an interface or
abstractmethod to a class without a default implementation. - Renaming or moving a package.
- Changing a class's superclass or removing an implemented interface.
- Removing or renaming an enum constant.
- Renaming a service-file key, or removing a previously-published service implementation (see "Service files" below).
These are also breaking, even though japicmp may not catch them:
- Behavioural changes that users have come to rely on, including MetaClass dispatch, method-resolution order, GDK method semantics, and serialization formats.
- Changing the bytecode shape that
@CompileStaticproduces in a way that breaks reflection-based callers. - Changing the AST shape produced for a given source construct in a way that breaks third-party AST transformations.
- Changing the order or timing of compilation phases that user customizers attach to.
- Breaking Java interoperability — including changes that prevent Java code from calling compiled Groovy classes, that prevent Groovy code from calling existing Java libraries, that change joint-compilation semantics, or that change the shape of generated Java stubs. Seamless Java interop is a covenant of Groovy, not just a feature.
The bar for a breaking change is discussion on the dev list and a major version, not a single PR. If a change might be breaking, the safe default is to assume it is and ask on the list.
New API is the easier-to-fix mistake — you can deprecate and remove — but it's still costly. Before adding any:
- Justify the surface. Is this something one or many users have
asked for, or is it convenience for a single internal caller? If
the latter, make it package-private or
@Internal. - Pick the narrowest visibility that works. Default to package-private; widen only when the cross-package or cross-module need is real.
- Place it in
org.apache.groovy.*for new code. Useorg.codehaus.groovy.*only when the new symbol must integrate with existing internals there. - Consider marking the feature as incubating if the design is
still settling. Annotate API surfaces with
@Incubating; for features that aren't expressed as a class or method (grammar tweaks, cross-cutting behaviour), say "incubating" in the prose documentation instead. Either form buys room to refine in a minor release. See "Incubating features" above. - Document it. Public types need accurate Groovydoc/Javadoc; if
the feature is user-facing, also add or update an AsciiDoc section
under
src/spec/doc/or the relevant subproject'ssrc/spec/doc/. - Add tests. Both unit tests and, where appropriate, an
executable spec example under
src/spec/test/.
When you need to remove or replace public API:
-
Mark the old symbol
@Deprecated(since = "X.Y.Z", forRemoval = true)when removal is planned, or@Deprecated(since = "X.Y.Z")when retirement is open-ended. -
In the Javadoc, name the replacement explicitly with
@deprecatedand a one-line pointer:/** * @deprecated since 5.1.0, use {@link #newMethod(String)} instead. */
-
Keep the deprecated symbol working — same semantics, no behaviour drift — until it is removed.
-
Remove deprecated symbols only in a major release, and only after a deprecation has shipped in at least one minor release first.
-
If a behavioural change is unavoidable in the same release as the deprecation (rare), call it out in the release notes.
Classic (non-invokedynamic) call-site classes under
org.codehaus.groovy.runtime.callsite were moved out of groovy-core
into the optional module groovy-callsite.
Primary purpose: keep runtime binary compatibility for classes
compiled by Groovy 4 and Groovy 5 (and earlier releases that always
used classic call sites). Those classes embed
$getCallSiteArray / CallSiteArray / CallSite linkage. With
org.apache.groovy:groovy-callsite on the runtime classpath, that
bytecode must still load and execute on Groovy 6. The published
module preserves the same public linkage surface that Groovy 4/5
classic bytecode depends on (CallSiteArray(Class, String[]), public
array / owner fields, and the full CallSite method set).
Core does not depend on that module; runtime dispatch uses invokedynamic
by default (as since Groovy 4). Related helpers that existed only to
construct classic call sites (for example MetaClassImpl#createPojoCallSite,
CachedMethod#createPojoMetaMethodSite, CachedClass#getCallSiteLoader)
were removed from core in this major version. Public method-selection
overloads remain on MetaClassImpl for the optional runtime without
package-private coupling.
To compile with indy disabled, or to run classic-mode classes
from older releases, add org.apache.groovy:groovy-callsite to the
classpath. Compiling with indy=false without that module fails fast
with a clear error rather than emitting unloadable bytecode. Valuable
helpers that used to live beside the call-site cache
(BooleanClosureWrapper and related types) remain in core under
org.codehaus.groovy.runtime.
The classic call-site types are not marked @Deprecated in 6.0.0
beta releases so downstream (notably Grails) can validate invokedynamic
performance without formal deprecation noise. Javadoc states they are
planned for deprecation/removal in a future Groovy version. Formal
@Deprecated may be restored before 6 GA once beta feedback confirms
indy remains acceptable for those use cases.
The @Immutable transform in Groovy 4.0.5 through 4.0.x emitted
references to org.apache.groovy.runtime.ObjectUtil.cloneObject into
generated constructors and getters for defensive copies of array,
Cloneable, and collection properties (GROOVY-10747). The class was
removed in 5.0.0-alpha-1 together with the $getLookup machinery it
relied on (GROOVY-10931), so those pre-compiled classes failed on
Groovy 5+ with NoClassDefFoundError — only at first execution of a
clone path, which for collection properties depends on the runtime
value being Cloneable, making the failure intermittent and easy to
miss in testing.
ObjectUtil is restored (5.1.1, 6.0.0) as a deprecated
binary-compatibility facade with the original semantics: arrays clone
via the unchanged ArrayUtil fast paths, other Cloneables through
their public clone() via the MOP, and a non-public clone() still
surfaces NoSuchMethodException exactly as before. Classes compiled
by ≤ 4.0.4 (which used ReflectionMethodInvoker, still present) and
by 5+ (whose transforms emit invokedynamic or InvokerHelper clone
calls) never reference the class. It exists only as a link target for
Groovy 4 bytecode and is not intended for direct use. This lets
"legacy" Grails plugins compiled with Groovy 4 still work with later
Grails versions using more recent Groovy versions.
Groovy 6 aligns JEP 394 / JLS §6.3-style pattern variable scoping for
e instanceof T t and e !instanceof T t (equivalent to
!(e instanceof T t)) with Java for the common shapes: if/else
(including abrupt-completion survivors), short-circuit && / ||,
ternary/Elvis arms, and true-path bindings in while bodies.
Who is affected (silent behaviour change in dynamic Groovy). Code that
previously treated a pattern variable as a local outside its JLS live range
— for example in an else branch of if (o instanceof String s), after a
fall-through if, or on the RHS of o instanceof String s || s.isEmpty() —
now resolves that name as a dynamic property. At runtime that yields
MissingPropertyException (or a type-checker error under @TypeChecked /
@CompileStatic), with no additional compile-time warning in dynamic
mode. That is the intended Java-aligned semantics; Groovy 6 is the major
version for the change.
What is not claimed. while / do-while deliberately get only
partial flow scoping: true-path bindings are available in a while body
and short-circuit rules apply in conditions, but Groovy does not
introduce false-path bindings after a loop when the body cannot complete
normally (JLS §6.3.2.3). Pattern names never leak past the loop. Full
after-loop introduction is a possible future enhancement, not a 6.0 gap to
back-port silently.
Release notes for the 6.0 beta line should call this out (JIRA
breaking label on GROOVY-12242).
Groovy 6 compiles switch expressions (JEP 361) as first-class AST
(SwitchExpression / YieldStatement) instead of desugaring them to an
immediately-called closure around a switch statement (the GROOVY-9272
implementation shipped in 4.x / 5.x). As in Java (JEP 361) the -> arm
only decides fall-through; the position decides whether a value is produced
(GROOVY-12399, GROOVY-12408). A switch whose value is not used is a
SwitchStatement, whatever its arm shape, and does nothing when unmatched.
That includes the last statement of a closure, a script or a method: the
implicit return applies to the statement, as it does to an if or a
colon-form switch. In expression position it is a SwitchExpression and
every path must yield or throw; write return switch (...) {...}, or
assign the result, to ask for that.
Who is affected (runtime behaviour). A switch expression whose selector
matches no arm throws IllegalStateException, where the 4.x / 5.x desugared
closure completed without a return and evaluated to null. A switch
statement does nothing, as before. The difference is visible where a method
body is a bare switch: it is a statement, so an unmatched selector leaves
the method's default value (null, or 0 / false for a primitive return
type) rather than the null a 5.x expression produced, and rather than the
GroovyCastException 5.x raised when coercing that null to a primitive.
Under @TypeChecked / @CompileStatic, a non-exhaustive switch expression
(no default, and not a complete enum) is a compile-time error; a statement
needs no default.
Who is affected (AST tools). Visitors, macros, and AST transforms that
assumed a switch expression was a MethodCallExpression wrapping a
SwitchStatement need to handle SwitchExpression and YieldStatement.
GroovyCodeVisitor supplies default methods so existing visitors keep
compiling. SwitchExpression.transformExpression rewrites the selector and
case-label expressions only and does not copy arm statements.
ClassCodeExpressionTransformer.transform stays generic — it
delegates to transformExpression, the same way it treats
ClosureExpression. ResolveVisitor, StaticImportVisitor and the static-compilation
transformer visit the node from their own transform override so
arm expressions still run through resolve, static-import rewrite
and static compilation.
A plain ExpressionTransformer leaves arm bodies untouched.
Under @CompileStatic, a non-intrinsic case label is resolved as
label.isCase(selector) and emitted as a direct method call. An
Object-typed label therefore uses isCase(Object, Object) (equals),
the same as a statically compiled label.isCase(selector) call. Dynamic
Groovy still dispatches isCase on the label's runtime class.
What is not claimed. Matching still uses Groovy isCase (Class, regex,
Collection, Closure). tableswitch / lookupswitch are emitted only when
the selector and labels are constants of a type javac would switch on.
CompilerConfiguration(CompilerConfiguration) now copies the source
configuration's compilation customizers along with every other setting.
Before Groovy 6 it copied everything except customizers.
Who is affected. Code that derives a configuration from an existing one and then registers its own customizers:
CompilerConfiguration child = new CompilerConfiguration(parent);
child.addCompilationCustomizers(mine); // now: parent's customizers *and* mineThe change is silent — no exception, just customizers running that
previously did not. An ImportCustomizer adds its imports twice; an
ASTTransformationCustomizer, which carries mutable applied state, is
invoked for a second compilation.
To restore the old behaviour, use the two-argument copy constructor added in 6.0.0:
CompilerConfiguration child = new CompilerConfiguration(parent, false);Nested compilation. A copied customizer is invoked for every primary
class node of the child compilation, including nodes added via
CompilationUnit.addClassNode, which have no SourceUnit — as that
method's javadoc warns. A customizer that dereferences the SourceUnit
it is handed will therefore throw NullPointerException; Gradle's
incremental-compilation customizer is one such, and Groovy's own
SourceAwareCustomizer is another. A customizer may equally assume the
class nodes it sees belong to the compilation it was registered for.
Groovy's own nested compilations —
StaticTypeCheckingSupport.evaluateExpression, which compiles a
synthetic expression holder, and GroovyTypeCheckingExtensionSupport,
which compiles a type checking DSL script — therefore pass false.
Prefer new CompilerConfiguration(parent, false) whenever you derive a
configuration for a nested compilation, and null-check the SourceUnit
in any customizer that might be applied to one.
A method (or constructor) whose own @TypeChecked or @CompileStatic
annotation has the default non-SKIP mode is now type checked — and for
@CompileStatic, statically compiled — even when its declaring class is
annotated with @CompileDynamic, @CompileStatic(TypeCheckingMode.SKIP)
or @TypeChecked(TypeCheckingMode.SKIP). Previously the class-level
SKIP silently won and the method-level annotation was ignored. Nested
classes already behaved this way (GROOVY-10238); this aligns methods
with them: the most specific annotation wins, and a class-level SKIP
is the default only for members without their own annotation.
Who is affected. Code with a method-level opt-in under a class-level
opt-out. Such methods may now raise type-checking errors that were
previously not reported, and their bodies are statically compiled where
they were previously dynamic. Remove the method-level annotation (or
change it to SKIP mode) to retain the old behaviour.
What is unchanged. The opt-out direction is untouched:
@CompileDynamic / SKIP-mode methods inside checked classes are
skipped exactly as before. Cross-family behaviour is also unchanged: a
method-level @CompileStatic(TypeCheckingMode.SKIP) disables static
compilation but does not exempt the method from an enclosing class's
@TypeChecked checking; only @TypeChecked(TypeCheckingMode.SKIP)
does that.
The compiler's error tolerance — the number of non-fatal errors collected
before compilation is abandoned, CompilerConfiguration.getTolerance(),
groovyc -t — is now also enforced for errors reported through
ClassCodeVisitorSupport#addError, which includes all static type
checking errors. It previously covered only errors reported through
SourceUnit#addError (parse and class generation); visitor errors went
straight to ErrorCollector#addErrorAndContinue and were unbounded.
The default is unchanged at 10 (now named
CompilerConfiguration.DEFAULT_TOLERANCE). A tolerance of zero or less
now means unlimited, which is the setting to reach for when every error
is wanted; previously zero was indistinguishable from the option being
absent on the command line, and would have bailed out on the first error
if set through the API.
Who is affected. Any compilation reporting more than the tolerance in
type checking errors. Where 40 such errors were previously all reported,
10 are now reported and compilation stops. Pass -t 0 on the groovyc
command line, set tolerance="0" on the Ant <groovyc> task, or set
configuration.tolerance = 0 in a compiler configuration script (the
route Gradle users have, via groovyOptions.configurationScript) to
restore full reporting.
This is not limited to the compiler front ends: it applies to any caller
driving a ClassCodeVisitorSupport subclass over a SourceUnit and
counting the errors it collects — static analysers, IDE integrations and
AST transformation test harnesses among them. Note in particular that
SourceUnit.create(String, String) selects a tolerance of 1, so a
visitor driven over a source unit from that factory now stops at the
first error. Use the three-argument overload to state the tolerance the
caller actually wants.
What is unchanged. The default of 10, so parse and class generation
errors behave exactly as before. The temporary error collectors that
StaticTypeCheckingVisitor pushes for speculative checks are also
unaffected — they must keep collecting without bailing out, because their
errors are routinely discarded once a candidate is ruled in or out, so
tolerance is enforced only against the source unit's own collector.
Errors that reach the collector without passing through an enforcing
path also behave as before: direct calls to
ErrorCollector#addErrorAndContinue — the route most AST
transformations take, via AbstractASTTransformation#addError — and
bulk merges via ErrorCollector#addCollectorContents (annotation
checking in ExtendedVerifier, for example) report their errors and
carry on regardless of the threshold. Such errors still count towards
the error total, so the next error reported through an enforcing path
takes them into account.
Completeness remains per-phase, as it always has been: failIfErrors()
runs at the end of each phase, so a single type checking error anywhere
in the compilation still suppresses every class generation error,
whatever the tolerance.
Coercing a String to a Class — 'com.example.Foo' as Class, and the
(Class) cast the compiler routes through
ShortTypeHandling.castToClass — now resolves the named class without
initializing it. Previously it called the single-argument
Class.forName(name), which runs the class's static initializer as a side
effect; it now uses Class.forName(name, false, loader) with the same class
loader as before. The class is still initialized lazily on first real use, as
the JVM always does.
Who is affected (runtime behaviour). Code that relied on the coercion to
trigger a static initializer — most commonly the legacy JDBC idiom
'com.mysql.jdbc.Driver' as Class to register a driver — no longer gets that
side effect. Call Class.forName(name) (or Class.forName(name, true, loader))
explicitly where the initialization is wanted; modern JDBC drivers auto-register
via ServiceLoader and do not need it. The @GroovyABI signature is unchanged,
so japicmp sees nothing — this is a behavioural change only.
Groovy 6 is the major version for the change. On 3.0.x/4.0.x/5.0.x/5.1.x the coercion still initializes the class.
The subprojects/binary-compatibility/
module wires
japicmp into the build. For
each library subproject, it compares the current repackageJar output
(shadow-relocated published jar) against the published artefact of a
baseline version and produces an HTML report.
| Aspect | Detail |
|---|---|
| Tool | me.champeau.gradle.japicmp Gradle plugin |
| Baseline version | binaryCompatibilityBaseline Gradle property (-PbinaryCompatibilityBaseline=5.0.4 or gradle.properties) |
| Coverage | All subprojects with groovyLibrary.checkBinaryCompatibility = true (the default) |
| Visibility checked | protected and above |
| Excluded | Packages matching **internal** and groovyjarjar**; closure classes and dgm$* runtime helpers |
| Where reports land | subprojects/<name>/build/reports/japicmp<Name>.html |
| Aggregating task | ./gradlew :binary-compatibility:checkBinaryCompatibility |
A few things to know about the check:
- It does not fail the build.
failOnModification = false— reports are produced, but a CI job has to read them and a human has to interpret them. The report is a guidance tool, not a gate. - The
@GroovyABI-marked subset can be gated. Because the marker is retained in class files, a stricter task variant can useannotationIncludes = ['@org.apache.groovy.lang.annotation.GroovyABI']withfailOnModification = trueto enforce the annotated subset while the broad report stays advisory. @Internalis not what excludes a symbol from the check. Exclusion is by package name (**internal**). The annotation is a documentation marker that helps Groovydoc, AST tools, and reviewers; to genuinely move something out of the binary-compatibility surface, aninternalpackage is the right home.- The baseline is a Gradle property, not pinned in code. When a
release is cut, the property is updated to the previous release.
Local runs default to whatever's in
gradle.properties.
If japicmp flags a change you intended:
- Confirm the affected symbol was actually public, not internal.
- Confirm the change is necessary — most "improvements" to public API are not.
- If it stays, raise it on the dev list. The release-management conversation is the one that signs off on what version absorbs the break.
These are public surface even though they don't show up in japicmp:
META-INF/services/org.codehaus.groovy.transform.ASTTransformation— global AST transformations.META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule— GDK-style extension modules adding methods to existing classes.- Anything published under
META-INF/services/orMETA-INF/groovy/.
Removing or renaming an entry in these files is as breaking as removing the corresponding type. New entries get added additively, the same way as a new public type.
ARCHITECTURE.md— overall layout; the package-conventions section there is summarised in this document and elaborated with stability semantics here.CONTRIBUTING.md— build, test, submission.GOVERNANCE.md— how breaking-change and API-addition decisions are taken to dev@; review modes and wait periods.AGENTS.md— AI-contributor supplement.src/spec/doc/version-scheme.adoc— user-facing version-numbering scheme.subprojects/binary-compatibility/build.gradle— exact configuration of the japicmp check.