Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/memory/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ See [README.md](README.md) for the format and routing rules.
- [kotlin-test-formatting](feedback/kotlin-test-formatting.md) — `@Nested` should be on the same line as `inner class`, and backticked name on the next line.
- [equals-tester](feedback/equals-tester.md) — Use Guava's `EqualsTester` for testing `equals()` and `hashCode()`.
- [utility-class-testing](feedback/utility-class-testing.md) — Use `UtilityClassTest` as the base for testing utility classes.
- [test-class-name-filter](feedback/test-class-name-filter.md) — Gradle `test` runs only `*Test`/`*Spec` classes; others are skipped silently.

## Project (durable context & rationale)

Expand Down
23 changes: 23 additions & 0 deletions .agents/memory/feedback/test-class-name-filter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
name: test-class-name-filter
description: Gradle `test` runs only classes named `*Test`/`*Spec`; others are skipped silently.
metadata:
type: feedback
since: 2026-08-08
---

The shared test convention (`registerTestTasks()` in
`buildSrc/src/main/kotlin/io/spine/gradle/testing/Tasks.kt`) filters test
execution to class names matching `*Test` or `*Spec` and sets
`filter.isFailOnNoMatchingTests = false`. A JUnit class with any other name
compiles, is reported as `BUILD SUCCESSFUL`, and never runs — even when
selected explicitly with `--tests`.

**Why:** During the Jackson 3 migration, a fixture-generating test named
`V2FixtureGeneration` silently did not run; the absence of its output files
was the only signal.

**How to apply:** Name every JUnit class `*Spec` (convention for specs) or
`*Test`. When a test run is expected to produce a side effect, verify the
side effect, not the exit code. If a `--tests` selection reports success
suspiciously fast, check `build/test-results/test/` for the class XML.
137 changes: 137 additions & 0 deletions .agents/tasks/bump-jackson.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# Migrate `base-libraries` to Jackson 3.2.1

Task brief: `config/docs/jackson-3-migration-brief.md` (authoritative copy supplied
by the user). Target: `tools.jackson:jackson-bom:3.2.1` — confirmed the newest
3.2.x on Maven Central (`latest`/`release` = 3.2.1, checked 2026-08-07).

The `config` repo already carries the migrated `Jackson` dependency object on its
`bump-jackson` branch (`dbfa5398`, pushed to origin). This repo consumes it via
the `config` submodule + `buildSrc` copy.

## Phase 0 inventory

### Modules declaring Jackson dependencies

Only `format` (`format/build.gradle.kts`):

- `platform(Jackson.bom)` — 2.22.1 → 3.2.1
- `implementation(databind)` — group moves to `tools.jackson.core`
- `implementation(DataFormat.yaml)` — group moves to `tools.jackson.dataformat`
- `implementation(DataType.jdk8)` — **remove**: merged into `jackson-databind` in 3.0
- `implementation(DataType.dateTime)` (jsr310) — **remove**: merged into databind
- `implementation(DataType.guava)` — stays (3.x line exists)
- `runtimeOnly(moduleKotlin)` — group moves to `tools.jackson.module`

`buildSrc` has its own decoupled `jacksonVersion = 2.18.3` (used for XML parsing
inside build logic); config@bump-jackson deliberately keeps it on 2.x — buildSrc
sources still use the `com.fasterxml.jackson.*` API.

### Sources using Jackson (all in `format`)

- `io.spine.format.JacksonSupport` — public `@SPI` base; holds
`internal abstract val factory: JsonFactory`, protected lazy
`mapper = ObjectMapper(factory)` (illegal in 3.x), public companion
`modules: MutableList<Module>` seeded by `ObjectMapper.findModules()`.
- `io.spine.format.parse.JacksonParser` (+ `JsonParser`, `YamlParser` objects).
- `io.spine.format.write.JacksonWriter` (+ `JsonWriter`, `YamlWriter` objects;
`JsonFactory()` / `YAMLFactory()` construction).

### Persistence / API boundaries

`format` is a library; it does not persist anything itself, but downstream Spine
SDK tools (compiler, tool-base, …) use `io.spine.format.write`/`parse` for
settings and interchange files (JSON/YAML). Wire format of *their* output changes
with 3.x defaults (property order, java.time representation). Guarded here by
2.22.1-generated fixtures + round-trip tests; downstream repos migrate separately
(brief §2.3) and their Jackson stays 2.x until then — formats coexist.

`ProtoBinary` / `ProtoJson` formats use protobuf-java, not Jackson — unaffected.

### Custom serializers / modules / IOException catch sites

- No custom `JsonSerializer`/`JsonDeserializer`/`Module`/modifier implementations.
- No `catch (IOException)` / `@Throws(IOException::class)` anywhere in module
sources. Only stale KDoc `@throws java.io.IOException` claims in `Parse.kt`
(4×) and `parse/Parser.kt` (1×) — to be corrected (3.x throws unchecked
`JacksonException`).
- §11 removals: none in use (`DataFormatDetector`, `canSerialize`,
`MappingJsonFactory`, jsonSchema, `ObjectCodec`, `JsonFactory.get/setCodec`).

## Plan

1. Submodule → config@bump-jackson; copy buildSrc delta (Jackson.kt,
buildSrc/build.gradle.kts comment). Commit (mechanical).
2. Still on 2.22.1: write deterministic JSON/YAML fixtures via
`io.spine.format.write` into `format/src/test/resources/…/v2/`. Commit.
3. Migrate `format` build deps + sources to `tools.jackson.*`, builder-based
immutable mappers (`JsonMapper.builder()` / `YAMLMapper.builder()`),
`Module` → `JacksonModule`, KDoc updates. Commit.
4. Add fixture round-trip tests (2.x file → 3.x parse) + capture 3.x output
diff vs 2.x baseline for the report. Commit.
5. Verify: `./gradlew build`, residual grep (only `com.fasterxml.jackson.annotation`
may remain), `:format:dependencies` (no unintended 2.x Jackson), Dokka.
6. Report per brief §14.

## Decisions log

- 3.x changed defaults: accepted the new defaults (no restores). The library has
no persisted contract of its own; only mapper config was `INDENT_OUTPUT`,
which `JacksonSupport` still enables explicitly.
`builderWithJackson2Defaults()` scaffold skipped — single mapper construction
site, small test surface; all tests passed on 3.x defaults directly.
- `JacksonSupport.factory` was `internal`, so it was replaced by
`internal abstract fun mapperBuilder(): MapperBuilder<*, *>` without breaking
external API; `modules: MutableList<Module>` is public — its element type
change (`Module` → `JacksonModule`) is an unavoidable breaking change of this
major migration (version already bumped on this branch).

## Results (verified 2026-08-08)

- Resolved: `jackson-bom` / `jackson-databind` / `jackson-module-kotlin` 3.2.1;
`jackson-annotations` 2.22 (BOM-resolved, the only `com.fasterxml` artifact
on the `:format` runtime classpath).
- Wire format: the **only** diff between 2.22.1 and 3.2.1 outputs of the fixture
value is `java.time.Instant` — numeric epoch → ISO-8601 string
(`DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS` now off). Property order is
unchanged for Kotlin data classes (constructor properties keep declaration
order); YAML output is otherwise byte-identical despite the snakeyaml-engine
switch (`---` marker and quoting preserved).
- `Jackson2CompatibilitySpec` proves 2.x-written JSON/YAML (numeric `Instant`)
parse intact under 3.2.1.
- `./gradlew build` and `dokkaGenerate` clean; residual
`com.fasterxml.jackson` grep: zero hits in module sources (this repo never
used the 2.x annotations package). No test deleted or disabled.
- IOException catch sites: none existed; 5 stale KDoc `@throws` claims fixed.
- §11 removals: none in use.
- Deferred: `buildSrc`'s own Jackson stays 2.x by design (config-owned);
jackson-module-kotlin 3.3.x raises the Kotlin floor to 2.2 — this repo is on
Kotlin 2.3.x already, so the next LTS bump is unblocked from that side.

## Follow-up folded into this branch: `JacksonSupport` SPI rework

Per the user's request (initially spawned as a separate task, then redirected
to this same branch):

- Manual module registration is dropped entirely (per the user's decision):
the former `public MutableList` companion — and the interim
`registerModule()` design — are replaced by `ServiceLoader`-only discovery.
Each mapper builder calls `MapperBuilder.findAndAddModules()`; modules are
contributed by exposing them as `ServiceLoader` services (e.g., via
`@AutoService(JacksonModule::class)`). `JacksonSupport` no longer has a
companion object; removing the published `modules` property is a breaking
change covered by the `.430` bump. Module discovery remains covered by the
JSON/YAML round-trip specs, which require the Kotlin and Guava modules.
- The `internal abstract mapperBuilder()` vs. `@SPI public class` tension is
resolved on the documentation side: [Format] is a **sealed** class, so new
formats can only be added inside the `format` module. The class KDoc now says
so instead of implying external extension. `mapperBuilder()` stays `internal`.
- Review fixes applied from three review agents (kotlin-engineer,
spine-code-review, review-docs): `YAMLMapper` KDoc sentence rewritten
(meaning-distorting attachment), `Instant`/ISO-8601 wire note added to the
`mapper` KDoc, `@throws JacksonException` extended with "or its subclass",
widow-line reflows, `@DisplayName` backticks, `javaClass.getResource`.
- Version re-bumped `.427` → `.430`: the branch carries breaking API changes
(`modules` type changed twice over: `MutableList<Module>` →
`List<JacksonModule>`), and the version policy rounds breaking changes up to
the next multiple of ten. Sanctioned re-bump: reclassification to a breaking
PR.
19 changes: 0 additions & 19 deletions .github/workflows/gradle-wrapper-validation.yml

This file was deleted.

2 changes: 1 addition & 1 deletion .idea/live-templates/README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 8 additions & 5 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,11 @@ repositories {
/**
* The version of Jackson used by `buildSrc`.
*
* Please keep this value in sync with [io.spine.dependency.lib.Jackson.version].
* It is not a requirement but would be good in terms of consistency.
* This value is deliberately decoupled from [io.spine.dependency.lib.Jackson.version],
* which now points to Jackson 3.x. The `buildSrc` sources still use the Jackson 2.x API
* (`com.fasterxml.jackson.*`), so they must stay on a 2.x version until they are migrated
* to `tools.jackson.*`. Any maintained 2.x release will do — bump this only when `buildSrc`
* itself needs a fix from a later 2.x, not to track the newest one.
*/
val jacksonVersion = "2.18.3"

Expand Down Expand Up @@ -132,7 +135,7 @@ val kotestJvmPluginVersion = "0.4.10"
/**
* @see [io.spine.dependency.test.Kover]
*/
val koverVersion = "0.9.8"
val koverVersion = "0.9.9"

/**
* The version of the Shadow Plugin.
Expand Down Expand Up @@ -222,7 +225,7 @@ dependOnBuildSrcJar()
/**
* Adds a dependency on a `buildSrc.jar`, iff:
* 1) the `src` folder is missing, and
* 2) `buildSrc.jar` is present in `buildSrc/` folder instead.
* 2) `buildSrc.jar` is present in the `buildSrc/` folder instead.
*
* This approach is used in the scope of integration testing.
*/
Expand All @@ -241,7 +244,7 @@ fun Project.dependOnBuildSrcJar() {
* Includes the `implementation` dependency on `artifactregistry-auth-common`,
* with the version defined in [googleAuthToolVersion].
*
* `artifactregistry-auth-common` has transitive dependency on Gson and Apache `commons-codec`.
* `artifactregistry-auth-common` has a transitive dependency on Gson and Apache `commons-codec`.
* Gson from version `2.8.6` until `2.8.9` is vulnerable to Deserialization of Untrusted Data
* (https://devhub.checkmarx.com/cve-details/CVE-2022-25647/).
*
Expand Down
2 changes: 1 addition & 1 deletion buildSrc/quality/checkstyle-suppressions.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" ?>

<!--
~ 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.
Expand Down
2 changes: 1 addition & 1 deletion buildSrc/quality/checkstyle.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" ?>

<!--
~ 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.
Expand Down
2 changes: 1 addition & 1 deletion buildSrc/settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
43 changes: 33 additions & 10 deletions buildSrc/src/main/kotlin/BuildExtensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import io.spine.dependency.build.ErrorProne
import io.spine.dependency.build.GradleDoctor
import io.spine.dependency.build.Ksp
import io.spine.dependency.build.PluginPublishPlugin
import io.spine.dependency.lib.JetBrainsAnnotations
import io.spine.dependency.lib.Protobuf
import io.spine.dependency.local.Compiler
import io.spine.dependency.local.CoreJvmCompiler
Expand All @@ -39,18 +40,20 @@ import io.spine.dependency.test.Kover
import io.spine.gradle.repo.standardToSpineSdk
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.ModuleDependency
import org.gradle.api.file.DuplicatesStrategy
import org.gradle.api.tasks.JavaExec
import org.gradle.jvm.tasks.Jar
import org.gradle.kotlin.dsl.ScriptHandlerScope
import org.gradle.kotlin.dsl.exclude
import org.gradle.plugin.use.PluginDependenciesSpec
import org.gradle.plugin.use.PluginDependencySpec

/**
* Provides shortcuts to reference our dependency objects.
*
* Dependency objects cannot be used under `plugins` section because `io` is a value
* declared in auto-generated `org.gradle.kotlin.dsl.PluginAccessors.kt` file.
* Dependency objects cannot be used under the `plugins` section because `io` is a value
* declared in the auto-generated `org.gradle.kotlin.dsl.PluginAccessors.kt` file.
* It conflicts with our own declarations.
*
* In such cases, a shortcut to apply a plugin can be created:
Expand Down Expand Up @@ -91,7 +94,7 @@ val ScriptHandlerScope.coreJvmCompiler: CoreJvmCompiler
* Shortcut to [CoreJvmCompiler] dependency object.
*
* This plugin is not published to Gradle Portal and cannot be applied directly to a project.
* Firstly, it should be put to buildscript's classpath and then applied by ID only.
* Firstly, it should be put to the buildscript's classpath and then applied by ID only.
*/
val PluginDependenciesSpec.coreJvmCompiler: CoreJvmCompiler
get() = CoreJvmCompiler
Expand All @@ -116,8 +119,8 @@ val PluginDependenciesSpec.spineCompiler: Compiler
/**
* Provides shortcuts for applying plugins from our dependency objects.
*
* Dependency objects cannot be used under `plugins` section because `io` is a value
* declared in auto-generated `org.gradle.kotlin.dsl.PluginAccessors.kt` file.
* Dependency objects cannot be used under the `plugins` section because `io` is a value
* declared in the auto-generated `org.gradle.kotlin.dsl.PluginAccessors.kt` file.
* It conflicts with our own declarations.
*
* Declaring of top-level shortcuts eliminates the need to apply plugins
Expand Down Expand Up @@ -177,10 +180,10 @@ fun Project.configureTaskDependencies() {
* Creates a dependency between the Gradle task of *this* name
* onto the task with `taskName`.
*
* If either of tasks does not exist in the enclosing `Project`,
* If either of the tasks does not exist in the enclosing `Project`,
* this method does nothing.
*
* This extension is kept local to `configureTaskDependencies` extension
* This extension is kept local to the `configureTaskDependencies` extension
* to prevent its direct usage from outside.
*/
fun String.dependOn(taskName: String) {
Expand Down Expand Up @@ -278,7 +281,7 @@ fun JavaExec.remoteDebug(enabled: Boolean = true) {
*
* @param enabled If `true` the task will be suspended.
* @throws IllegalStateException if the task with the given name is not found, or,
* if the taks is not of [JavaExec] type.
* if the task is not of [JavaExec] type.
*/
fun Project.setRemoteDebug(taskName: String, enabled: Boolean = true) {
val task = tasks.findByName(taskName)
Expand Down Expand Up @@ -324,7 +327,7 @@ fun Project.testFixturesSpineCompilerRemoteDebug(enabled: Boolean = true) =
/**
* Parts of names of configurations to be excluded by
* `artifactMeta/excludeConfigurations/containing` in the modules
* where `io.spine.atifact-meta` plugin is applied.
* where the `io.spine.atifact-meta` plugin is applied.
*/
val buildToolConfigurations: Array<String> = arrayOf(
"detekt",
Expand All @@ -337,7 +340,7 @@ val buildToolConfigurations: Array<String> = arrayOf(
)

/**
* Make the `sourcesJar` task accept duplicated input, which seems to occur
* Makes the `sourcesJar` task accept duplicated input, which seems to occur
* somewhere inside Protobuf Gradle Plugin.
*/
fun Project.allowDuplicationInSourcesJar() {
Expand All @@ -347,3 +350,23 @@ fun Project.allowDuplicationInSourcesJar() {
}
}
}

/**
* Excludes `org.jetbrains:annotations` from this published dependency.
*
* Build script classpaths pin the module to the version used by the Kotlin
* runtime embedded into Gradle (`strictly 13.0`, "Pinned to the embedded
* Kotlin"), while `kotlinx-coroutines` and other transitive dependencies require
* later versions such as `23.0.0`.
* Gradle 9.6 may fail to reconcile the two declarations — the outcome depends on
* the shape of the consumer's dependency graph — making the plugin unresolvable
* without a consumer-side workaround, such as forcing the module version on
* the build script classpath.
*
* The annotations are compile-time metadata, not needed at runtime.
* Consumers still receive version `13.0` through the `kotlin-stdlib`
* dependency, which satisfies the pin.
*/
fun ModuleDependency.excludeJetBrainsAnnotations() {
exclude(group = JetBrainsAnnotations.groupId, module = JetBrainsAnnotations.artifactId)
}
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/BuildSettings.kt
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/DependencyResolution.kt
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ private fun ResolutionStrategy.forceTestDependencies() {
}

/**
* Forces transitive dependencies of 3rd party components that we don't use directly.
* Forces transitive dependencies of 3rd-party components that we don't use directly.
*/
private fun ResolutionStrategy.forceTransitiveDependencies() {
force(
Expand Down
Loading
Loading