, vararg params: Any): H {
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt
index 56221d57c..974542c3f 100644
--- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt
@@ -318,7 +318,7 @@ internal fun TaskContainer.getOrCreate(name: String, init: Jar.() -> Unit): Task
* Obtains as a set of [Jar] tasks, output of which is used as Maven artifacts.
*
* By default, only a jar with Java compilation output is included into publication. This method
- * registers tasks which produce additional artifacts according to the values of [jarFlags].
+ * registers tasks that produce additional artifacts according to the values of [jarFlags].
*
* @return the list of the registered tasks.
*/
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/SpinePublishing.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/SpinePublishing.kt
index a12c1d20f..bb412ed63 100644
--- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/SpinePublishing.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/SpinePublishing.kt
@@ -65,7 +65,7 @@ import org.gradle.kotlin.dsl.findByType
* ### Filtering out test-only modules
*
* Sometimes a functional or an integration test requires a significant amount of
- * configuration code which is better understood when isolated into a separate module.
+ * configuration code that is better understood when isolated into a separate module.
* Conventionally, we use the `-tests` suffix for naming such modules.
*
* In order to avoid publishing of such a test-only module, we use the following extensions
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repositories.kt b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repositories.kt
index e6c3f677d..8877dc615 100644
--- a/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repositories.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repositories.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2025, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -31,6 +31,7 @@ package io.spine.gradle.repo
import io.spine.gradle.publish.PublishingRepos
import java.net.URI
import org.gradle.api.artifacts.dsl.RepositoryHandler
+import org.gradle.api.artifacts.repositories.ArtifactRepository
import org.gradle.api.artifacts.repositories.MavenArtifactRepository
import org.gradle.kotlin.dsl.maven
@@ -94,24 +95,34 @@ fun RepositoryHandler.spineArtifacts(): MavenArtifactRepository = maven {
}
val RepositoryHandler.intellijReleases: MavenArtifactRepository
- get() = maven("https://www.jetbrains.com/intellij-repository/releases")
-
-val RepositoryHandler.jetBrainsCacheRedirector: MavenArtifactRepository
- get() = maven("https://cache-redirector.jetbrains.com/intellij-dependencies")
+ get() = maven("https://www.jetbrains.com/intellij-repository/releases") {
+ includeIntelliJPlatformOnly()
+ }
val RepositoryHandler.intellijDependencies: MavenArtifactRepository
get() = maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies") {
- content {
- includeGroupByRegex("com\\.jetbrains.*")
- includeGroupByRegex("org\\.jetbrains.*")
- includeGroupByRegex("com\\.intellij.*")
- }
+ includeIntelliJPlatformOnly()
}
/**
* Applies repositories commonly used by Spine Event Engine projects.
*/
fun RepositoryHandler.standardToSpineSdk() {
+ //
+ // General-purpose, highly available repositories come first. Gradle stops at
+ // the first repository that can serve an artifact, so keeping these ahead of
+ // the special-purpose ones means coordinates shared with them (such as
+ // `org.jetbrains:annotations`) resolve here and never reach a less reliable
+ // JetBrains mirror.
+ //
+ // `io.spine.*` modules are served only by the Spine repositories below, so
+ // they are excluded here. Otherwise Gradle would query Central / the Plugin
+ // Portal for every Spine module first, adding pointless lookups and making
+ // Spine resolution depend on the health of repositories that never host it.
+ //
+ mavenCentral { excludeSpine() }
+ gradlePluginPortal { excludeSpine() }
+
spineArtifacts()
@Suppress("DEPRECATION") // Still use `CloudRepo` for earlier versions.
@@ -131,16 +142,22 @@ fun RepositoryHandler.standardToSpineSdk() {
}
}
+ // IntelliJ Platform repositories. `intellijReleases` serves the platform
+ // artifacts (`com.jetbrains.intellij.*`); `intellijDependencies` serves the
+ // repackaged third-party dependencies (`org.jetbrains.intellij.deps.*` and
+ // JetBrains-internal builds). Each is restricted to the coordinates it serves
+ // (see `includeIntelliJPlatformOnly`), so a transient 5xx from one of them
+ // cannot break the resolution of unrelated artifacts.
intellijReleases
- jetBrainsCacheRedirector
intellijDependencies
maven {
url = URI(Repos.sonatypeSnapshots)
+ // This repository only ever serves snapshots; restrict it so it is not
+ // queried (and cannot fail the build) for release artifacts.
+ mavenContent { snapshotsOnly() }
}
- mavenCentral()
- gradlePluginPortal()
mavenLocal().includeSpineOnly()
}
@@ -180,3 +197,35 @@ private fun MavenArtifactRepository.includeSpineOnly() {
includeGroupByRegex("io\\.spine.*")
}
}
+
+/**
+ * Excludes Spine artifact groups from this repository.
+ *
+ * `io.spine.*` modules are published only to the Spine repositories (each scoped
+ * via [includeSpineOnly]). Excluding them from a general-purpose repository keeps
+ * Gradle from querying it — and depending on its health — for coordinates it
+ * never hosts.
+ */
+private fun ArtifactRepository.excludeSpine() {
+ content {
+ excludeGroupByRegex("io\\.spine.*")
+ }
+}
+
+/**
+ * Restricts a JetBrains/IntelliJ Platform repository to the coordinates it
+ * actually serves.
+ *
+ * These hosts periodically answer with HTTP 5xx. Once Gradle sees such an error,
+ * it disables the repository for the rest of the build and fails the resolution
+ * instead of falling back to another repository. Without this filter such a
+ * repository is queried for every artifact, so a single 502 on an unrelated POM
+ * (such as `com.fasterxml.jackson:jackson-parent`) would break the whole build.
+ */
+private fun MavenArtifactRepository.includeIntelliJPlatformOnly() {
+ content {
+ includeGroupByRegex("com\\.jetbrains.*")
+ includeGroupByRegex("org\\.jetbrains.*")
+ includeGroupByRegex("com\\.intellij.*")
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repository.kt b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repository.kt
index a586ffdb9..d345725b0 100644
--- a/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repository.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repository.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2025, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -33,12 +33,12 @@ import org.gradle.api.Project
/**
* A Maven repository.
*
- * @param name The human-readable name which is also used in the publishing task names
+ * @param name The human-readable name that is also used in the publishing task names
* for identifying the target repository.
* The name must match the [regex].
* @param releases The URL for publishing release versions of artifacts.
* @param snapshots The URL for publishing [snapshot][io.spine.gradle.isSnapshot] versions.
- * @param credentialsFile The path to the file which contains the credentials for the registry.
+ * @param credentialsFile The path to the file that contains the credentials for the registry.
* @param credentialValues The function to obtain an instance of [Credentials] from
* a Gradle [Project], if [credentialsFile] is not specified.
*/
@@ -116,7 +116,7 @@ data class Repository(
val password = properties.getProperty("user.password")
return Credentials(username, password)
}
-
+
override fun equals(other: Any?): Boolean = when {
this === other -> true
other !is Repository -> false
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/license/LicenseReporter.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/LicenseReporter.kt
index 596be9c68..aa7e65f44 100644
--- a/buildSrc/src/main/kotlin/io/spine/gradle/report/license/LicenseReporter.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/LicenseReporter.kt
@@ -64,7 +64,7 @@ import org.gradle.kotlin.dsl.the
object LicenseReporter {
/**
- * The name of the Gradle task which generates the reports for a specific Gradle project.
+ * The name of the Gradle task that generates the reports for a specific Gradle project.
*/
private const val projectTaskName = "generateLicenseReport"
@@ -96,6 +96,20 @@ object LicenseReporter {
renderers = arrayOf(MarkdownReportRenderer(Paths.outputFilename))
}
+
+ // The rendered report embeds the project's Maven coordinates — including its
+ // version — in the report header (see `Template.writeHeader`). The
+ // `generateLicenseReport` task is a `@CacheableTask` that keys its up-to-date check
+ // and build-cache entry on the resolved dependencies only, not on the project version.
+ // Without the version as an explicit input, a version-only change leaves the task
+ // `UP-TO-DATE` (or restorable from the build cache), so the report keeps the previous
+ // version while `pom.xml`, produced by an always-running task, is updated. Declaring
+ // the version as an input invalidates the cached output when it changes, so the report
+ // is regenerated. The value is read lazily so it reflects the version resolved at
+ // execution time, regardless of when `project.version` is assigned during configuration.
+ project.tasks.generateLicenseReport.configure {
+ inputs.property("projectVersion", project.provider { project.version.toString() })
+ }
}
/**
@@ -104,7 +118,7 @@ object LicenseReporter {
*
* The merge result is placed according to [Paths].
*
- * Registers a `mergeAllLicenseReports` which is specified to be executed after `build`.
+ * Registers a `mergeAllLicenseReports` that is specified to be executed after `build`.
*/
fun mergeAllReports(project: Project) {
val rootProject = project.rootProject
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/license/ModuleDataExtensions.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/ModuleDataExtensions.kt
index 91247e242..e8518fd20 100644
--- a/buildSrc/src/main/kotlin/io/spine/gradle/report/license/ModuleDataExtensions.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/ModuleDataExtensions.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2025, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -92,7 +92,7 @@ private fun MarkdownDocument.print(
}
/**
- * Prints the URL to the project which provides the dependency.
+ * Prints the URL to the project that provides the dependency.
*
* If the passed project URL is `null` or empty, it is not printed.
*/
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/license/Paths.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/Paths.kt
index 1d1632752..3c750b31a 100644
--- a/buildSrc/src/main/kotlin/io/spine/gradle/report/license/Paths.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/Paths.kt
@@ -46,7 +46,7 @@ internal object Paths {
* as the result of the [LicenseReporter] work.
*
* Its contents describe the licensing information for each of the Java dependencies
- * which are referenced by Gradle projects in the repository.
+ * that are referenced by Gradle projects in the repository.
*/
internal const val outputFilename = "dependencies.md"
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyWriter.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyWriter.kt
index 79d00c629..db2bf761a 100644
--- a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyWriter.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyWriter.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2025, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -54,7 +54,9 @@ import org.gradle.kotlin.dsl.withGroovyBuilder
* ```
*
* When there are several versions of the same dependency, only the one with
- * the newest version is retained.
+ * the newest version is retained. If the retained version is used in several
+ * configurations, the highest-ranking Maven scope is reported, e.g. `compile`
+ * wins over `test`.
*
* @see PomGenerator
*/
@@ -65,7 +67,7 @@ private constructor(
internal companion object {
/**
- * Creates the `ProjectDependenciesAsXml` for the passed [project].
+ * Creates the `DependencyWriter` for the passed [project].
*/
fun of(project: Project): DependencyWriter {
return DependencyWriter(project.dependencies())
@@ -75,7 +77,7 @@ private constructor(
/**
* Writes the dependencies in their `pom.xml` format to the passed [out] writer.
*
- * Used writer will not be closed.
+ * The used writer will not be closed.
*/
fun writeXmlTo(out: Writer) {
val xml = MarkupBuilder(out)
@@ -86,7 +88,12 @@ private constructor(
"dependency" {
"groupId" { xml.text(dependency.group) }
"artifactId" { xml.text(dependency.name) }
- "version" { xml.text(dependency.version) }
+ // A BOM-managed dependency carries no explicit version.
+ // Omit the element rather than emit `null`,
+ // since `null` is not a valid Maven version.
+ dependency.version?.let { version ->
+ "version" { xml.text(version) }
+ }
if (scopedDep.hasDefinedScope()) {
"scope" { xml.text(scopedDep.scopeName()) }
}
@@ -170,17 +177,30 @@ private fun Dependency.isExternal(): Boolean {
* But for our `pom.xml`, which has clearly representative character, a single version
* of a dependency is quite enough.
*
+ * Versions are compared by [VersionComparator] rather than as plain text, so `10.0.0`
+ * is recognized as newer than `9.2.0`, and `2.0.0-SNAPSHOT.100` — as newer
+ * than `2.0.0-SNAPSHOT.99`.
+ *
+ * When the newest version comes from several configurations, the occurrence with
+ * the highest-ranking Maven scope (as defined by [ScopedDependency.dependencyPriority])
+ * is retained. For example, a dependency declared via `api` in one module and via
+ * `testImplementation` in another is reported with the `compile` scope, so a production
+ * dependency is not misrepresented as a test-scoped one. Likewise, an artifact coming
+ * from `compileOnly` or `annotationProcessor` in one module and from a test
+ * configuration in another is reported as `provided`.
+ *
* The rejected duplicates are logged.
*/
private fun Project.deduplicate(dependencies: Set): List {
- val groups = dependencies.distinctBy { it.gav }
- .groupBy { it.run { "$group:$name" } }
+ val groups = dependencies.groupBy { it.run { "$group:$name" } }
- logDuplicates(groups)
+ logDuplicates(groups.mapValues { (_, deps) -> deps.distinctBy { it.gav } })
- val filtered = groups.map { group ->
- group.value.maxByOrNull { dep -> dep.version ?: "" }
- }.filterNotNull()
+ val filtered = groups.values.map { sameArtifact ->
+ val newest = sameArtifact.maxWith(compareBy(VersionComparator) { it.version ?: "" })
+ sameArtifact.filter { it.version == newest.version }
+ .minBy { it.scoped.dependencyPriority() }
+ }
return filtered
}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/ScopedDependency.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/ScopedDependency.kt
index 7c67a32ca..c969ce500 100644
--- a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/ScopedDependency.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/ScopedDependency.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2025, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -29,6 +29,7 @@ package io.spine.gradle.report.pom
import io.spine.gradle.report.pom.DependencyScope.compile
import io.spine.gradle.report.pom.DependencyScope.provided
import io.spine.gradle.report.pom.DependencyScope.runtime
+import io.spine.gradle.report.pom.DependencyScope.system
import io.spine.gradle.report.pom.DependencyScope.test
import io.spine.gradle.report.pom.DependencyScope.undefined
import org.gradle.api.artifacts.Configuration
@@ -106,17 +107,18 @@ private constructor(
configurationName.startsWith("test", ignoreCase = true)
/**
- * Performs comparison of {@code DependencyWithScope} instances according to these rules:
+ * Performs comparison of `ScopedDependency` instances according to these rules:
*
- * * Compares the scope of the dependency first. Dependency with lower scope priority
- * number goes first.
+ * * Compares the scope of the dependency first. Dependency with a lower scope priority
+ * number goes first.
*
- * * For dependencies with same scope, does the lexicographical group name comparison.
+ * * For dependencies with the **same scope** does the lexicographical group
+ * name comparison.
*
- * * For dependencies within the same group, does the lexicographical artifact
+ * * For dependencies within the **same group**, does the lexicographical artifact
* name comparison.
*
- * * For dependencies with the same artifact name, does the lexicographical artifact
+ * * For dependencies with the **same artifact name**, does the lexicographical artifact
* version comparison.
*/
private val COMPARATOR: Comparator =
@@ -138,7 +140,7 @@ private constructor(
return dependency
}
- /** Obtains the scope name of this dependency .*/
+ /** Obtains the scope name of this dependency. */
fun scopeName(): String {
return scope.name
}
@@ -147,14 +149,24 @@ private constructor(
* Obtains the layout priority of a scope.
*
* Layout priority determines what scopes come first in the generated `pom.xml` file.
- * Dependencies with a lower priority number go on top.
+ * Dependencies with a lower priority number go on top, following the conventional
+ * Maven scope order: `compile`, `provided`, `runtime`, `test`, and `system`.
+ * Dependencies with an undefined scope go last.
+ *
+ * The same ordering also drives the scope selection when the same dependency
+ * comes from several configurations: the occurrence with the lowest priority
+ * number is reported. So, a scope required by production code wins over `test`,
+ * and a known scope wins over an undefined one.
*/
+ @Suppress("MagicNumber") // Reason: the values encode the relative scope order.
internal fun dependencyPriority(): Int {
return when (scope) {
compile -> 0
- runtime -> 1
- test -> 2
- else -> 3
+ provided -> 1
+ runtime -> 2
+ test -> 3
+ system -> 4
+ undefined -> 5
}
}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/VersionComparator.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/VersionComparator.kt
new file mode 100644
index 000000000..c6984bb65
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/VersionComparator.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.gradle.report.pom
+
+/**
+ * Compares dependency version strings by their meaning rather than lexicographically.
+ *
+ * Numeric segments are ordered as numbers, so `10.0.0` is newer than `9.2.0`, and
+ * `2.0.0-SNAPSHOT.100` is newer than `2.0.0-SNAPSHOT.99`. A plain `String` comparison
+ * would order both pairs the other way around.
+ *
+ * The rules follow Semantic Versioning where it applies:
+ *
+ * 1. A version consists of a release part and an optional qualifier, separated by
+ * the first `-`: for `2.0.0-SNAPSHOT.100` these are `2.0.0` and `SNAPSHOT.100`.
+ * 2. Both parts are compared segment by segment, as split by `.`, and also by `-`
+ * within a qualifier. Two numeric segments are compared as numbers, two textual
+ * ones as case-insensitive text, and a numeric segment is older than a textual one.
+ * 3. When one version runs out of segments, it is the older one: `1.0.1` is newer
+ * than `1.0`, and `1.0.0-RC.1` is newer than `1.0.0-RC`.
+ * 4. When the release parts are equal, a version without a qualifier is newer than
+ * a version with one: `2.0.0` is newer than `2.0.0-SNAPSHOT.100`.
+ *
+ * Unlike full Maven semantics, qualifiers carry no special meaning: `RC`, `SNAPSHOT`,
+ * and the like are ordered as plain text. This keeps the comparison simple and
+ * predictable for the report, where only the relative recency of the versions
+ * of the same artifact matters.
+ */
+internal object VersionComparator : Comparator {
+
+ override fun compare(left: String, right: String): Int {
+ val (leftRelease, leftQualifier) = left.parse()
+ val (rightRelease, rightQualifier) = right.parse()
+ val byRelease = compareSegments(leftRelease, rightRelease)
+ if (byRelease != 0) {
+ return byRelease
+ }
+ return when {
+ leftQualifier == null && rightQualifier == null -> 0
+ leftQualifier == null -> 1
+ rightQualifier == null -> -1
+ else -> compareSegments(leftQualifier, rightQualifier)
+ }
+ }
+
+ /**
+ * Splits this version into the segments of its release part and the segments
+ * of its qualifier, the latter being `null` when the version has no qualifier.
+ */
+ private fun String.parse(): Pair, List?> {
+ val release = substringBefore('-')
+ val qualifier = if ('-' in this) substringAfter('-') else null
+ return release.split('.') to qualifier?.split('.', '-')
+ }
+
+ private fun compareSegments(left: List, right: List): Int {
+ for (index in 0 until maxOf(left.size, right.size)) {
+ val bySegment = compareSegment(
+ left.getOrElse(index) { "" },
+ right.getOrElse(index) { "" }
+ )
+ if (bySegment != 0) {
+ return bySegment
+ }
+ }
+ return 0
+ }
+
+ /**
+ * Compares single segments, ordering an absent (empty) segment below any present
+ * one, a numeric segment below a textual one, numbers by their value, and text
+ * case-insensitively.
+ *
+ * Keeping the empty, numeric, and textual segments in distinct buckets makes
+ * the order transitive: comparing a numeric pair as numbers, but a mixed pair
+ * as text, would order `2` < `10` < `1a` < `2`.
+ */
+ private fun compareSegment(left: String, right: String): Int {
+ if (left.isEmpty() || right.isEmpty()) {
+ return left.length.compareTo(right.length)
+ }
+ val leftNumber = left.toLongOrNull()
+ val rightNumber = right.toLongOrNull()
+ return when {
+ leftNumber != null && rightNumber != null -> leftNumber.compareTo(rightNumber)
+ leftNumber != null -> -1
+ rightNumber != null -> 1
+ else -> left.compareTo(right, ignoreCase = true)
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitCoverage.kt b/buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitCoverage.kt
index 96f90bb2b..9682596d8 100644
--- a/buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitCoverage.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitCoverage.kt
@@ -127,7 +127,7 @@ internal const val TESTKIT_COVERAGE_DIR: String = "jacoco-testkit"
/**
* The name of the system property carrying the absolute path to the JaCoCo
- * agent JAR which the test harness attaches to TestKit worker JVMs.
+ * agent JAR that the test harness attaches to TestKit worker JVMs.
*
* The value is read by `plugin-testlib` at test runtime.
*
diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/report/pom/DependencyWriterSpec.kt b/buildSrc/src/test/kotlin/io/spine/gradle/report/pom/DependencyWriterSpec.kt
new file mode 100644
index 000000000..0c4b23355
--- /dev/null
+++ b/buildSrc/src/test/kotlin/io/spine/gradle/report/pom/DependencyWriterSpec.kt
@@ -0,0 +1,312 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.gradle.report.pom
+
+import io.kotest.matchers.ints.shouldBeGreaterThan
+import io.kotest.matchers.ints.shouldBeLessThan
+import io.kotest.matchers.shouldBe
+import io.kotest.matchers.string.shouldContain
+import io.kotest.matchers.string.shouldNotContain
+import java.io.StringWriter
+import org.gradle.api.Project
+import org.gradle.testfixtures.ProjectBuilder
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+
+@DisplayName("`DependencyWriter` should")
+internal class DependencyWriterSpec {
+
+ private val rootProject: Project = ProjectBuilder.builder().withName("root").build()
+
+ /**
+ * Creates a subproject of the [rootProject] with the given name.
+ *
+ * The names of the subprojects in the tests below are chosen so that
+ * a module using a dependency in a lower-ranked scope — as defined by
+ * [ScopedDependency.dependencyPriority] — sorts first, and is thus
+ * encountered first when the dependencies are collected. This way,
+ * the tests prove that the merged scope does not depend on the order
+ * in which project modules are traversed.
+ */
+ private fun subproject(name: String): Project =
+ ProjectBuilder.builder().withParent(rootProject).withName(name).build()
+
+ /**
+ * Declares a dependency with the given [notation] in the named [configuration],
+ * creating it if it does not exist.
+ */
+ private fun Project.declare(configuration: String, notation: String) {
+ configurations.maybeCreate(configuration)
+ dependencies.add(configuration, notation)
+ }
+
+ @Nested inner class
+ `merge an artifact duplicated across modules` {
+
+ @Test
+ fun `preferring the 'compile' scope over the 'test' one`() {
+ subproject("a-tests").declare("testImplementation", SPINE_BASE)
+ subproject("b-lib").declare("api", SPINE_BASE)
+
+ val dependency = rootProject.dependencies().single()
+
+ dependency.scopeName() shouldBe DependencyScope.compile.name
+ }
+
+ @Test
+ fun `preferring the 'runtime' scope over the 'test' one`() {
+ subproject("a-tests").declare("testImplementation", SPINE_BASE)
+ subproject("b-lib").declare("runtimeOnly", SPINE_BASE)
+
+ val dependency = rootProject.dependencies().single()
+
+ dependency.scopeName() shouldBe DependencyScope.runtime.name
+ }
+
+ @Test
+ fun `preferring the 'compile' scope over the 'runtime' one`() {
+ subproject("a-run").declare("runtimeOnly", SPINE_BASE)
+ subproject("b-lib").declare("implementation", SPINE_BASE)
+
+ val dependency = rootProject.dependencies().single()
+
+ dependency.scopeName() shouldBe DependencyScope.compile.name
+ }
+
+ @Test
+ fun `preferring the 'provided' scope over the 'test' one`() {
+ subproject("a-tests").declare("testImplementation", SPINE_BASE)
+ subproject("b-lib").declare("compileOnly", SPINE_BASE)
+
+ val dependency = rootProject.dependencies().single()
+
+ dependency.scopeName() shouldBe DependencyScope.provided.name
+ }
+
+ @Test
+ fun `reporting 'annotationProcessor' and 'testAnnotationProcessor' usages as 'provided'`() {
+ subproject("a-tests").declare("testAnnotationProcessor", SPINE_BASE)
+ subproject("b-codegen").declare("annotationProcessor", SPINE_BASE)
+
+ val dependency = rootProject.dependencies().single()
+
+ dependency.scopeName() shouldBe DependencyScope.provided.name
+ }
+
+ @Test
+ fun `preferring the 'compile' scope over the 'provided' one`() {
+ subproject("a-tools").declare("compileOnly", SPINE_BASE)
+ subproject("b-lib").declare("implementation", SPINE_BASE)
+
+ val dependency = rootProject.dependencies().single()
+
+ dependency.scopeName() shouldBe DependencyScope.compile.name
+ }
+
+ @Test
+ fun `preferring the 'provided' scope over the 'runtime' one`() {
+ subproject("a-run").declare("runtimeOnly", SPINE_BASE)
+ subproject("b-tools").declare("compileOnly", SPINE_BASE)
+
+ val dependency = rootProject.dependencies().single()
+
+ dependency.scopeName() shouldBe DependencyScope.provided.name
+ }
+
+ @Test
+ fun `retaining the newest version with the widest of its scopes`() {
+ subproject("a-tests").declare("testImplementation", SPINE_BASE_NEWER)
+ subproject("b-lib").declare("api", SPINE_BASE_NEWER)
+ subproject("c-old").declare("api", SPINE_BASE)
+
+ val dependency = rootProject.dependencies().single()
+
+ dependency.dependency().version shouldBe "2.0.1"
+ dependency.scopeName() shouldBe DependencyScope.compile.name
+ }
+
+ @Test
+ fun `comparing versions semantically rather than as text`() {
+ subproject("a-lib").declare("api", "io.spine:spine-base:9.2.0")
+ subproject("b-lib").declare("api", "io.spine:spine-base:10.0.0")
+
+ val dependency = rootProject.dependencies().single()
+
+ dependency.dependency().version shouldBe "10.0.0"
+ }
+
+ @Test
+ fun `ordering pre-release increments numerically`() {
+ subproject("a-old").declare("api", "io.spine:spine-base:2.0.0-SNAPSHOT.99")
+ subproject("b-new").declare("api", "io.spine:spine-base:2.0.0-SNAPSHOT.100")
+
+ val dependency = rootProject.dependencies().single()
+
+ dependency.dependency().version shouldBe "2.0.0-SNAPSHOT.100"
+ }
+
+ @Test
+ fun `preferring a release over its pre-release`() {
+ subproject("a-snapshot").declare("api", "io.spine:spine-base:2.0.0-SNAPSHOT.100")
+ subproject("b-release").declare("api", SPINE_BASE)
+
+ val dependency = rootProject.dependencies().single()
+
+ dependency.dependency().version shouldBe "2.0.0"
+ }
+
+ /**
+ * The `api` usage of the older `9.2.0` must affect neither the version
+ * nor the scope: both come from the usages of the newest `10.0.0`,
+ * which would lose to `9.2.0` in a plain text comparison.
+ */
+ @Test
+ fun `taking the widest scope from the usages of the numerically newest version`() {
+ subproject("a-lib").declare("api", "io.spine:spine-base:9.2.0")
+ subproject("b-tests").declare("testImplementation", "io.spine:spine-base:10.0.0")
+ subproject("c-run").declare("runtimeOnly", "io.spine:spine-base:10.0.0")
+
+ val dependency = rootProject.dependencies().single()
+
+ dependency.dependency().version shouldBe "10.0.0"
+ dependency.scopeName() shouldBe DependencyScope.runtime.name
+ }
+
+ /**
+ * When the newest version of an artifact occurs only in test configurations,
+ * the `test` scope is reported even if an older version is a production
+ * dependency: the report describes the retained version as it is used.
+ */
+ @Test
+ fun `taking the scope only from the usages of the newest version`() {
+ subproject("a-tests").declare("testImplementation", SPINE_BASE_NEWER)
+ subproject("b-lib").declare("api", SPINE_BASE)
+
+ val dependency = rootProject.dependencies().single()
+
+ dependency.dependency().version shouldBe "2.0.1"
+ dependency.scopeName() shouldBe DependencyScope.test.name
+ }
+
+ @Test
+ fun `keeping the 'test' scope for an artifact used only in tests`() {
+ subproject("a-tests").declare("testImplementation", SPINE_BASE)
+ subproject("b-tests").declare("testRuntimeOnly", SPINE_BASE)
+
+ val dependency = rootProject.dependencies().single()
+
+ dependency.scopeName() shouldBe DependencyScope.test.name
+ }
+
+ @Test
+ fun `preferring a known scope over that of an unknown configuration`() {
+ subproject("a-tools").declare("spineCompiler", SPINE_BASE)
+ subproject("b-tests").declare("testImplementation", SPINE_BASE)
+
+ val dependency = rootProject.dependencies().single()
+
+ dependency.hasDefinedScope() shouldBe true
+ dependency.scopeName() shouldBe DependencyScope.test.name
+ }
+
+ @Test
+ fun `preferring the 'provided' scope over that of an unknown configuration`() {
+ subproject("a-tools").declare("spineCompiler", SPINE_BASE)
+ subproject("b-lib").declare("compileOnly", SPINE_BASE)
+
+ val dependency = rootProject.dependencies().single()
+
+ dependency.hasDefinedScope() shouldBe true
+ dependency.scopeName() shouldBe DependencyScope.provided.name
+ }
+ }
+
+ @Test
+ fun `omit the scope of a dependency coming only from an unknown configuration`() {
+ subproject("lib").declare("spineCompiler", SPINE_BASE)
+
+ val dependency = rootProject.dependencies().single()
+
+ dependency.hasDefinedScope() shouldBe false
+ }
+
+ @Test
+ fun `omit the version of a dependency that declares none`() {
+ subproject("a-bom").declare("api", "io.grpc:grpc-stub")
+ subproject("b-lib").declare("api", SPINE_BASE)
+
+ val out = StringWriter()
+ DependencyWriter.of(rootProject).writeXmlTo(out)
+ val xml = out.toString()
+
+ xml shouldContain "grpc-stub"
+ xml shouldNotContain "null"
+ xml shouldContain "2.0.0"
+ }
+
+ @Test
+ fun `write a production dependency as 'compile' even when it is also used in tests`() {
+ subproject("a-tests").declare("testImplementation", SPINE_BASE)
+ subproject("b-lib").declare("api", SPINE_BASE)
+
+ val out = StringWriter()
+ DependencyWriter.of(rootProject).writeXmlTo(out)
+ val xml = out.toString()
+
+ xml shouldContain "spine-base"
+ xml shouldContain "compile"
+ xml shouldNotContain "test"
+ }
+
+ @Test
+ fun `lay out dependencies in the conventional Maven scope order`() {
+ subproject("a-tests").declare("testImplementation", "io.spine:spine-testlib:2.0.0")
+ subproject("b-run").declare("runtimeOnly", "io.spine:spine-logging:2.0.0")
+ subproject("c-tools").declare("annotationProcessor", "io.spine:spine-validate:2.0.0")
+ subproject("d-lib").declare("api", SPINE_BASE)
+
+ val out = StringWriter()
+ DependencyWriter.of(rootProject).writeXmlTo(out)
+ val xml = out.toString()
+
+ val compileAt = xml.indexOf("compile")
+ val providedAt = xml.indexOf("provided")
+ val runtimeAt = xml.indexOf("runtime")
+ val testAt = xml.indexOf("test")
+
+ compileAt shouldBeGreaterThan -1
+ compileAt shouldBeLessThan providedAt
+ providedAt shouldBeLessThan runtimeAt
+ runtimeAt shouldBeLessThan testAt
+ }
+
+ private companion object {
+ const val SPINE_BASE = "io.spine:spine-base:2.0.0"
+ const val SPINE_BASE_NEWER = "io.spine:spine-base:2.0.1"
+ }
+}
diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/report/pom/VersionComparatorSpec.kt b/buildSrc/src/test/kotlin/io/spine/gradle/report/pom/VersionComparatorSpec.kt
new file mode 100644
index 000000000..54b643950
--- /dev/null
+++ b/buildSrc/src/test/kotlin/io/spine/gradle/report/pom/VersionComparatorSpec.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.gradle.report.pom
+
+import io.kotest.matchers.ints.shouldBeGreaterThan
+import io.kotest.matchers.ints.shouldBeLessThan
+import io.kotest.matchers.shouldBe
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Test
+
+@DisplayName("`VersionComparator` should")
+internal class VersionComparatorSpec {
+
+ /**
+ * Asserts that [newer] compares above [older], checking both directions.
+ */
+ private fun assertNewer(newer: String, older: String) {
+ VersionComparator.compare(newer, older) shouldBeGreaterThan 0
+ VersionComparator.compare(older, newer) shouldBeLessThan 0
+ }
+
+ @Test
+ fun `compare numeric segments as numbers`() {
+ assertNewer("10.0.0", "9.2.0")
+ assertNewer("2.10.0", "2.9.1")
+ assertNewer("1.0.10", "1.0.9")
+ }
+
+ @Test
+ fun `compare numeric qualifier segments as numbers`() {
+ assertNewer("2.0.0-SNAPSHOT.100", "2.0.0-SNAPSHOT.99")
+ assertNewer("2.0.0-SNAPSHOT.100", "2.0.0-SNAPSHOT.070")
+ }
+
+ @Test
+ fun `treat a release as newer than its pre-release`() {
+ assertNewer("2.0.0", "2.0.0-SNAPSHOT.100")
+ assertNewer("1.0.0", "1.0.0-RC.2")
+ }
+
+ @Test
+ fun `treat a longer version as newer when the common segments are equal`() {
+ assertNewer("1.0.1", "1.0")
+ assertNewer("1.0.0-RC.1", "1.0.0-RC")
+ }
+
+ @Test
+ fun `ignore the case of textual segments`() {
+ assertNewer("1.0.0-snapshot.10", "1.0.0-SNAPSHOT.2")
+ VersionComparator.compare("1.0.0-RC", "1.0.0-rc") shouldBe 0
+ }
+
+ @Test
+ fun `order a numeric segment before a textual one`() {
+ assertNewer("1.0.0-alpha", "1.0.0-1")
+ }
+
+ @Test
+ fun `treat equal versions as equal`() {
+ VersionComparator.compare("2.0.0-SNAPSHOT.070", "2.0.0-SNAPSHOT.070") shouldBe 0
+ VersionComparator.compare("31.1-jre", "31.1-jre") shouldBe 0
+ }
+}
diff --git a/config b/config
index 234233e0f..89ddc7591 160000
--- a/config
+++ b/config
@@ -1 +1 @@
-Subproject commit 234233e0fa407df296ff4742887723653c3dcc95
+Subproject commit 89ddc75919d75cc9ceee8822c2fdc93a24784e8b
diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md
index c935deb65..d0ec489fe 100644
--- a/docs/dependencies/dependencies.md
+++ b/docs/dependencies/dependencies.md
@@ -1,6 +1,6 @@
-# Dependencies of `io.spine.tools:classic-codegen:2.0.0-SNAPSHOT.400`
+# Dependencies of `io.spine.tools:classic-codegen:2.0.0-SNAPSHOT.401`
## Runtime
1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2.
@@ -828,14 +828,14 @@
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using
+This report was generated on **Fri Jun 19 11:10:55 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:gradle-plugin-api:2.0.0-SNAPSHOT.400`
+# Dependencies of `io.spine.tools:gradle-plugin-api:2.0.0-SNAPSHOT.401`
## Runtime
1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.0.
@@ -1734,14 +1734,14 @@ This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using
+This report was generated on **Fri Jun 19 11:10:55 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:gradle-plugin-api-test-fixtures:2.0.0-SNAPSHOT.400`
+# Dependencies of `io.spine.tools:gradle-plugin-api-test-fixtures:2.0.0-SNAPSHOT.401`
## Runtime
1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.0.
@@ -2212,14 +2212,14 @@ This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using
+This report was generated on **Fri Jun 19 11:10:54 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:gradle-root-plugin:2.0.0-SNAPSHOT.400`
+# Dependencies of `io.spine.tools:gradle-root-plugin:2.0.0-SNAPSHOT.401`
## Runtime
1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2.
@@ -3070,14 +3070,14 @@ This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using
+This report was generated on **Fri Jun 19 11:10:55 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:intellij-platform:2.0.0-SNAPSHOT.400`
+# Dependencies of `io.spine.tools:intellij-platform:2.0.0-SNAPSHOT.401`
## Runtime
1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3.
@@ -4151,14 +4151,14 @@ This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Wed Jun 10 18:49:06 WEST 2026** using
+This report was generated on **Fri Jun 19 11:10:55 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:intellij-platform-java:2.0.0-SNAPSHOT.400`
+# Dependencies of `io.spine.tools:intellij-platform-java:2.0.0-SNAPSHOT.401`
## Runtime
1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3.
@@ -5930,14 +5930,14 @@ This report was generated on **Wed Jun 10 18:49:06 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Wed Jun 10 18:49:06 WEST 2026** using
+This report was generated on **Fri Jun 19 11:10:56 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:jvm-tool-plugins:2.0.0-SNAPSHOT.400`
+# Dependencies of `io.spine.tools:jvm-tool-plugins:2.0.0-SNAPSHOT.401`
## Runtime
1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2.
@@ -6780,14 +6780,14 @@ This report was generated on **Wed Jun 10 18:49:06 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using
+This report was generated on **Fri Jun 19 11:10:55 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:jvm-tools:2.0.0-SNAPSHOT.400`
+# Dependencies of `io.spine.tools:jvm-tools:2.0.0-SNAPSHOT.401`
## Runtime
1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.1.0.
@@ -7547,14 +7547,14 @@ This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using
+This report was generated on **Fri Jun 19 11:10:55 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:plugin-base:2.0.0-SNAPSHOT.400`
+# Dependencies of `io.spine.tools:plugin-base:2.0.0-SNAPSHOT.401`
## Runtime
1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2.
@@ -8405,14 +8405,14 @@ This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using
+This report was generated on **Fri Jun 19 11:10:55 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:plugin-testlib:2.0.0-SNAPSHOT.400`
+# Dependencies of `io.spine.tools:plugin-testlib:2.0.0-SNAPSHOT.401`
## Runtime
1. **Group** : com.google.auto.value. **Name** : auto-value-annotations. **Version** : 1.11.1.
@@ -9367,14 +9367,14 @@ This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using
+This report was generated on **Fri Jun 19 11:10:55 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:protobuf-setup-plugins:2.0.0-SNAPSHOT.400`
+# Dependencies of `io.spine.tools:protobuf-setup-plugins:2.0.0-SNAPSHOT.401`
## Runtime
1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2.
@@ -10237,14 +10237,14 @@ This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using
+This report was generated on **Fri Jun 19 11:10:55 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:psi:2.0.0-SNAPSHOT.400`
+# Dependencies of `io.spine.tools:psi:2.0.0-SNAPSHOT.401`
## Runtime
1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3.
@@ -11345,14 +11345,14 @@ This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Wed Jun 10 18:49:06 WEST 2026** using
+This report was generated on **Fri Jun 19 11:10:55 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:psi-java:2.0.0-SNAPSHOT.400`
+# Dependencies of `io.spine.tools:psi-java:2.0.0-SNAPSHOT.401`
## Runtime
1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3.
@@ -13167,14 +13167,14 @@ This report was generated on **Wed Jun 10 18:49:06 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Wed Jun 10 18:49:06 WEST 2026** using
+This report was generated on **Fri Jun 19 11:10:56 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:tool-base:2.0.0-SNAPSHOT.400`
+# Dependencies of `io.spine.tools:tool-base:2.0.0-SNAPSHOT.401`
## Runtime
1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2.
@@ -14054,6 +14054,6 @@ This report was generated on **Wed Jun 10 18:49:06 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using
+This report was generated on **Fri Jun 19 11:10:55 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
\ No newline at end of file
diff --git a/docs/dependencies/pom.xml b/docs/dependencies/pom.xml
index a7b7e58f0..43922c44a 100644
--- a/docs/dependencies/pom.xml
+++ b/docs/dependencies/pom.xml
@@ -10,7 +10,7 @@ all modules and does not describe the project structure per-subproject.
-->
io.spine.tools
tool-base
-2.0.0-SNAPSHOT.400
+2.0.0-SNAPSHOT.401
2015
@@ -35,6 +35,12 @@ all modules and does not describe the project structure per-subproject.
33.6.0-jre
compile
+
+ com.google.protobuf
+ protobuf-gradle-plugin
+ 0.10.0
+ compile
+
com.google.protobuf
protobuf-java
@@ -170,7 +176,7 @@ all modules and does not describe the project structure per-subproject.
io.spine
spine-base
- 2.0.0-SNAPSHOT.404
+ 2.0.0-SNAPSHOT.420
compile
@@ -179,6 +185,12 @@ all modules and does not describe the project structure per-subproject.
2.0.0-SNAPSHOT.417
compile
+
+ io.spine.tools
+ base-testlib
+ 2.0.0-SNAPSHOT.213
+ compile
+
org.jboss.forge.roaster
roaster-api
@@ -197,6 +209,12 @@ all modules and does not describe the project structure per-subproject.
2.3.21
compile
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+ 2.3.21
+ compile
+
org.jetbrains.kotlin
kotlin-stdlib
@@ -221,6 +239,42 @@ all modules and does not describe the project structure per-subproject.
1.0.0
compile
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 6.1.0
+ compile
+
+
+ com.google.code.findbugs
+ jsr305
+ 3.0.2
+ provided
+
+
+ com.google.errorprone
+ error_prone_annotations
+ 2.36.0
+ provided
+
+
+ com.google.errorprone
+ error_prone_type_annotations
+ 2.36.0
+ provided
+
+
+ org.checkerframework
+ checker-qual
+ 4.2.0
+ provided
+
+
+ org.jetbrains.kotlin
+ kotlin-gradle-plugin-api
+ 2.3.21
+ provided
+
com.google.guava
guava-testlib
@@ -258,9 +312,9 @@ all modules and does not describe the project structure per-subproject.
test
- io.spine.tools
- base-testlib
- 2.0.0-SNAPSHOT.213
+ org.jacoco
+ org.jacoco.agent
+ 0.8.15
test
@@ -275,12 +329,6 @@ all modules and does not describe the project structure per-subproject.
2.3.0
test
-
- org.junit.jupiter
- junit-jupiter-api
- 6.1.0
- test
-
org.junit.jupiter
junit-jupiter-engine
@@ -293,35 +341,11 @@ all modules and does not describe the project structure per-subproject.
6.1.0
test
-
- com.google.code.findbugs
- jsr305
- 3.0.2
- provided
-
-
- com.google.errorprone
- error_prone_annotations
- 2.36.0
- provided
-
com.google.errorprone
error_prone_core
2.36.0
-
- com.google.errorprone
- error_prone_type_annotations
- 2.36.0
- provided
-
-
- com.google.protobuf
- protobuf-gradle-plugin
- 0.10.0
- provided
-
com.google.protobuf
protoc
@@ -362,17 +386,6 @@ all modules and does not describe the project structure per-subproject.
pmd-java
7.25.0
-
- org.checkerframework
- checker-qual
- 4.2.0
- provided
-
-
- org.jacoco
- org.jacoco.agent
- 0.8.15
-
org.jacoco
org.jacoco.report
@@ -423,17 +436,6 @@ all modules and does not describe the project structure per-subproject.
kotlin-build-tools-impl
2.3.21
-
- org.jetbrains.kotlin
- kotlin-gradle-plugin-api
- 2.3.21
- provided
-
-
- org.jetbrains.kotlin
- kotlin-reflect
- 2.3.21
-
org.jetbrains.kotlin
kotlin-sam-with-receiver-compiler-plugin-embeddable
diff --git a/gradle.properties b/gradle.properties
index d9d857b8d..7c2bb5f20 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -10,18 +10,26 @@ org.gradle.parallel=true
# Reuse task outputs from the local build cache.
# On CI, `gradle/actions/setup-gradle` persists `caches/build-cache-1` across runs,
# so cold builds skip work whose inputs are unchanged.
-#
-# Disabled for now: this repository's own build applies the *published*
-# `protobuf-setup-plugins` (see the root `build.gradle.kts` buildscript classpath),
-# which does not yet declare `generated/` and `desc.ref` as outputs of
-# `generateProto`. With the cache on, a `clean build` restores `generateProto` from
-# the cache without re-running its `doLast` actions, leaving the generated code
-# missing (e.g., in `classic-codegen`). Re-enable after `ToolBase.version` in
-# `buildSrc` points to a version containing the fix.
-#org.gradle.caching=true
+org.gradle.caching=true
-# Dokka plugin eats more memory than usual. Therefore, all builds should have enough.
-org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -XX:+UseParallelGC -Dfile.encoding=UTF-8
+# Extra JVM args for the Gradle daemon, for two unrelated reasons:
+#
+# 1. The Dokka plugin eats more memory than usual, so all builds get a generous heap.
+# 2. The `--add-exports` / `--add-opens` flags expose the `jdk.compiler` internals that
+# Error Prone needs on JDK 16+ (JEP 396). Passing them to the daemon here lets the
+# `net.ltgt.errorprone` plugin run Error Prone in-process instead of forking a separate
+# compiler JVM per task. See https://github.com/SpineEventEngine/config/issues/543
+org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -XX:+UseParallelGC -Dfile.encoding=UTF-8 \
+ --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
+ --add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
+ --add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \
+ --add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED \
+ --add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
+ --add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED \
+ --add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
+ --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
+ --add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED \
+ --add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
# suppress inspection "UnusedProperty"
# The below property enables generation of XML reports for tests.
diff --git a/init-submodules b/init-submodules
new file mode 100755
index 000000000..0c12a2817
--- /dev/null
+++ b/init-submodules
@@ -0,0 +1,87 @@
+#!/usr/bin/env bash
+
+################################################################################
+#
+# Materialize the *config-managed* submodules a fresh working tree is missing, so
+# agent assets resolve.
+#
+# `git worktree add` — and some shallow CI / cloud checkouts — populate only the
+# superproject's own tracked files; registered submodules are left UNinitialized.
+# In a Spine repo that means the `config` and `.agents/shared` submodules are
+# empty, the `.agents/skills` -> `.agents/shared/skills` symlink dangles, and no
+# agent skills, scripts, or guidelines can be found.
+#
+# This script is the bootstrap that has to run BEFORE `./config/pull`: `pull`
+# lives inside the `config` submodule, so on a fresh worktree it does not yet
+# exist. `init-submodules`, by contrast, is a plain tracked file at the repo root
+# (distributed by `config`), so `git worktree add` always checks it out — it can
+# therefore bring `config` itself into existence.
+#
+# It initializes ONLY submodules that are BOTH:
+#
+# * not yet checked out — those `git submodule status` marks with a leading `-`,
+# at the commit the branch pins; and
+#
+# * config-managed — `config` itself (the bootstrap target `pull` lives inside,
+# which carries no tracked `branch` in a consumer's `.gitmodules`), plus every
+# submodule that declares a tracked `branch` in `.gitmodules`. This is exactly
+# the rule `./config/pull` uses to decide what it floats, so the two scripts
+# can never disagree about what is shared.
+#
+# Consumer-owned submodules (a Hugo theme, a vendored library, documentation
+# examples, ...) declare no tracked branch and are deliberately left untouched.
+# Because a `SessionStart` hook runs this script automatically on every session,
+# initializing them would mean trying to clone — or failing on credentials for —
+# a submodule this project does not manage, on every single start. They are
+# skipped (noted on stderr).
+#
+# Submodules already present are left exactly as they are, so a tree that floated
+# `config` / `.agents/shared` to a branch tip via `./config/pull` is never
+# silently rewound to the pin. That makes the script idempotent and safe to run on
+# every session start.
+#
+# It does NOT float submodules to their branch tips — run `./config/pull`
+# afterwards for that. Unlike `pull`, it depends on no pre-existing `config`
+# submodule, so it can bootstrap a bare worktree where `./config/pull` does not
+# yet exist.
+#
+################################################################################
+
+set -u
+
+root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
+cd "$root" || exit 0
+
+# Nothing to do in a repo without submodules.
+[ -f .gitmodules ] || exit 0
+
+# The set of config-managed submodule paths: `config` itself (handled specially —
+# it carries no tracked branch, exactly as in `./config/pull`), plus every
+# submodule declaring a tracked `branch` in `.gitmodules`. Mirrors `pull`'s rule.
+config_managed_paths() {
+ printf '%s\n' 'config'
+ git config -f .gitmodules --get-regexp '^submodule\..*\.branch$' 2>/dev/null \
+ | while read -r key _branch; do
+ name=${key#submodule.}; name=${name%.branch}
+ git config -f .gitmodules --get "submodule.$name.path" 2>/dev/null
+ done
+}
+
+managed=$(config_managed_paths | sort -u)
+
+# `git submodule status` prefixes each uninitialized submodule with `-`; an
+# initialized one starts with a space (at the pinned commit) or `+` (ahead of it).
+# Act only on the `-` lines, taking the path from the second field, and only when
+# that path is config-managed.
+git submodule status 2>/dev/null | awk '$1 ~ /^-/ { print $2 }' | while read -r path; do
+ [ -n "$path" ] || continue
+ if printf '%s\n' "$managed" | grep -qxF -- "$path"; then
+ echo "init-submodules: initializing '$path'"
+ git submodule update --init --recursive -- "$path" \
+ || echo "init-submodules: WARNING — could not initialize '$path' (offline?)." >&2
+ else
+ echo "init-submodules: skipping consumer-owned '$path' (not config-managed)." >&2
+ fi
+done
+
+exit 0
diff --git a/version.gradle.kts b/version.gradle.kts
index 30c92175f..be50dc957 100644
--- a/version.gradle.kts
+++ b/version.gradle.kts
@@ -24,4 +24,4 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-val versionToPublish: String by extra("2.0.0-SNAPSHOT.400")
+val versionToPublish: String by extra("2.0.0-SNAPSHOT.401")