diff --git a/.agents/_TOC.md b/.agents/_TOC.md deleted file mode 100644 index 4be0656bdf..0000000000 --- a/.agents/_TOC.md +++ /dev/null @@ -1,23 +0,0 @@ -# Table of Contents - -1. [Quick Reference Card](quick-reference-card.md) -2. [JVM project requirements](jvm-project.md) โ€” language, build, and review checklist shared by all JVM repos -3. [Coding guidelines](coding-guidelines.md) -4. [Documentation & comments](documentation-guidelines.md) -5. [Documentation tasks](documentation-tasks.md) -6. [Running builds](running-builds.md) -7. [Version policy](version-policy.md) -8. [Project structure expectations](project-structure-expectations.md) -9. [Testing](testing.md) -10. [Safety rules](safety-rules.md) -11. [Advanced safety rules](advanced-safety-rules.md) -12. [Refactoring guidelines](refactoring-guidelines.md) -13. [Common tasks](common-tasks.md) -14. [Team memory](memory/MEMORY.md) -15. [Task plans](tasks/README.md) -16. [Java to Kotlin conversion](skills/java-to-kotlin/SKILL.md) -17. [Dependency update](skills/dependency-update/SKILL.md) -18. [Documentation review](skills/review-docs/SKILL.md) -19. [Pre-PR checklist](skills/pre-pr/SKILL.md) -20. [Kotlin code review](skills/kotlin-review/SKILL.md) -21. [Dependency audit](skills/dependency-audit/SKILL.md) diff --git a/.agents/advanced-safety-rules.md b/.agents/advanced-safety-rules.md deleted file mode 100644 index e4105813fc..0000000000 --- a/.agents/advanced-safety-rules.md +++ /dev/null @@ -1,6 +0,0 @@ -# ๐Ÿšจ Advanced safety rules - -- Do **not** auto-update external dependencies without explicit request. -- Do **not** inject analytics or telemetry code. -- Flag any usage of unsafe constructs (e.g., reflection, I/O on the main thread). -- Avoid generating blocking calls inside coroutines. diff --git a/.agents/coding-guidelines.md b/.agents/coding-guidelines.md deleted file mode 100644 index 8c0a60f34a..0000000000 --- a/.agents/coding-guidelines.md +++ /dev/null @@ -1,41 +0,0 @@ -# ๐Ÿงพ Coding guidelines - -## Core principles - -- Adhere to [Spine Event Engine Documentation][spine-docs] for coding style. -- Generate code that compiles cleanly and passes static analysis. -- Respect existing architecture, naming conventions, and project structure. -- Write clear, incremental commits with descriptive messages. -- Include automated tests for any code change that alters functionality. - -## Kotlin best practices - -### โœ… Prefer -- **Kotlin idioms** over Java-style approaches: - - Extension functions - - `when` expressions - - Smart casts - - Data classes and sealed classes - - Immutable data structures -- **Simple nouns** over composite nouns (`user` > `userAccount`) -- **Generic parameters** over explicit variable types (`val list = mutableList()`) -- **Java interop annotations** only when needed (`@file:JvmName`, `@JvmStatic`) -- **Kotlin DSL** for Gradle files -- **Kotlin Protobuf DSL** (`myMessage { field = value }`) over Java builder chains - -### โŒ Avoid -- Mutable data structures -- Java-style verbosity (builders with setters) -- Java Protobuf builders in Kotlin code (`newBuilder()`, `toBuilder()`) unless interop requires them -- Redundant null checks (`?.let` misuse) -- Using `!!` unless clearly justified -- Type names in variable names (`userObject`, `itemList`) -- String duplication (use constants in companion objects) -- Mixing Groovy and Kotlin DSLs in build logic -- Reflection unless specifically requested - -## Text formatting - - โœ… Replace double empty lines with a single empty line in the code. - - โœ… Remove trailing space characters in the code. - -[spine-docs]: https://github.com/SpineEventEngine/documentation/wiki diff --git a/.agents/common-tasks.md b/.agents/common-tasks.md deleted file mode 100644 index 5ee954d835..0000000000 --- a/.agents/common-tasks.md +++ /dev/null @@ -1,6 +0,0 @@ -# ๐Ÿ“‹ Common tasks - -- **Adding a new dependency**: Update relevant files in `buildSrc` directory. -- **Creating a new module**: Follow existing module structure patterns. -- **Documentation**: Use KDoc style for public and internal APIs. -- **Testing**: Create comprehensive tests using Kotest assertions. diff --git a/.agents/documentation-guidelines.md b/.agents/documentation-guidelines.md deleted file mode 100644 index 58a64a396d..0000000000 --- a/.agents/documentation-guidelines.md +++ /dev/null @@ -1,19 +0,0 @@ -# Documentation & comments - -## Commenting guidelines -- Avoid inline comments in production code unless necessary. -- Inline comments are helpful in tests. -- When using TODO comments, follow the format on the [dedicated page][todo-comments]. -- File and directory names should be formatted as code. - -## Protobuf file headers -- In `.proto` files, a multi-paragraph documentation header must end with a - trailing empty comment line (`//`). -- Single-paragraph headers do not require the trailing empty comment line. - -## Avoid widows, runts, orphans, or rivers - -Agents should **AVOID** text flow patters illustrated -on [this diagram](widow-runt-orphan.jpg). - -[todo-comments]: https://github.com/SpineEventEngine/documentation/wiki/TODO-comments diff --git a/.agents/documentation-tasks.md b/.agents/documentation-tasks.md deleted file mode 100644 index 8ac4660dbe..0000000000 --- a/.agents/documentation-tasks.md +++ /dev/null @@ -1,20 +0,0 @@ -# ๐Ÿ“„ Documentation tasks - -1. Ensure all public and internal APIs have KDoc examples. -2. Add in-line code blocks for clarity in tests. -3. Convert inline API comments in Java to KDoc in Kotlin: - ```java - // Literal string to be inlined whenever a placeholder references a non-existent argument. - private final String missingArgumentMessage = "[MISSING ARGUMENT]"; - ``` - transforms to: - ```kotlin - /** - * Literal string to be inlined whenever a placeholder references a non-existent argument. - */ - private val missingArgumentMessage = "[MISSING ARGUMENT]" - ``` - -4. Javadoc -> KDoc conversion tasks: - - Remove `

` tags in the line with text: `"

This"` -> `"This"`. - - Replace `

` with empty line if the tag is the only text in the line. diff --git a/.agents/guidelines b/.agents/guidelines new file mode 120000 index 0000000000..6f9d96637c --- /dev/null +++ b/.agents/guidelines @@ -0,0 +1 @@ +shared/guidelines \ No newline at end of file diff --git a/.agents/jvm-project.md b/.agents/jvm-project.md deleted file mode 100644 index e3c5d650d1..0000000000 --- a/.agents/jvm-project.md +++ /dev/null @@ -1,37 +0,0 @@ -# JVM Project Requirements - -General requirements for all JVM projects in the Spine SDK organisation. -Repo-specific `project.md` files link here and add their own context. - -## Language and build - -- **Languages**: Kotlin (primary), Java (secondary). -- **Build**: Gradle with Kotlin DSL. -- **Static analysis**: detekt, ErrorProne, Checkstyle, PMD. -- **Testing**: JUnit 5, Kotest Assertions, Codecov. - -## Code review checklist - -**Correctness and safety** -- Code compiles and passes static analysis (detekt, ErrorProne, Checkstyle, PMD). -- No reflection or unsafe code unless explicitly approved in scope. -- No analytics, telemetry, or tracking code. -- No blocking calls inside coroutines. - -**Kotlin/Java style** -- Kotlin idioms preferred: extension functions, `when` expressions, data/sealed - classes, immutable data structures. -- No `!!` unless provably safe. No unchecked casts. -- No mutable state without justification. -- No string duplication โ€” use constants. - -**Tests** -- New or changed functionality must include tests. -- Use stubs, not mocks. -- Prefer [Kotest assertions][kotest-assertions] over JUnit or Google Truth. - -**Versioning** -- If the repo has `version.gradle.kts`, every PR must include a version bump. - Flag the absence as a required change. - -[kotest-assertions]: https://kotest.io/docs/assertions/assertions.html diff --git a/.agents/memory/MEMORY.md b/.agents/memory/MEMORY.md index 2c8045c6ed..2cd56e83e3 100644 --- a/.agents/memory/MEMORY.md +++ b/.agents/memory/MEMORY.md @@ -6,6 +6,9 @@ See [README.md](README.md) for the format and routing rules. ## Feedback (validated patterns & corrections) - [copilot-review-request](feedback/copilot-review-request.md) โ€” GraphQL `requestReviews` with `botIds: ["BOT_kgDOCnlnWA"]`; REST endpoint silently no-ops on re-requests. +- [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. ## Project (durable context & rationale) diff --git a/.agents/memory/feedback/kotlin-test-formatting.md b/.agents/memory/feedback/kotlin-test-formatting.md new file mode 100644 index 0000000000..cd9a66042b --- /dev/null +++ b/.agents/memory/feedback/kotlin-test-formatting.md @@ -0,0 +1,23 @@ +# Kotlin Test Formatting + +## Backticked inner classes + +When using backticked descriptive names for inner classes in Kotlin tests: +1. The `@Nested` annotation must be on the same line as the `inner class` declaration. +2. The backticked class name must be on the next line. + +### Correct example: +```kotlin +@Nested internal inner class +`some descriptive name` { + // ... +} +``` + +### Incorrect example: +```kotlin +@Nested +internal inner class `some descriptive name` { + // ... +} +``` diff --git a/.agents/memory/feedback/utility-class-testing.md b/.agents/memory/feedback/utility-class-testing.md new file mode 100644 index 0000000000..2a16f4a254 --- /dev/null +++ b/.agents/memory/feedback/utility-class-testing.md @@ -0,0 +1,38 @@ +# Utility class testing + +In Spine libraries, utility classes (classes with only static methods and a private constructor) should be tested using `UtilityClassTest` as a base class. + +## Why + +`UtilityClassTest` automatically: +1. Verifies that the class is `final`. +2. Verifies that it has exactly one private constructor. +3. Verifies that the constructor throws an exception (usually `AssertionError`) or simply that it can be instantiated via reflection (depending on the implementation of `UtilityClassTest`), thus covering the private constructor for coverage purposes. + +## How + +### Kotlin + +Inherit from `UtilityClassTest(TargetClass::class.java)`: + +```kotlin +@DisplayName("`MyUtils` should") +internal class MyUtilsSpec : UtilityClassTest(MyUtils::class.java) { + // ... tests ... +} +``` + +### Java + +Inherit from `UtilityClassTest`: + +```java +@DisplayName("`MyUtils` should") +class MyUtilsTest extends UtilityClassTest { + + MyUtilsTest() { + super(MyUtils.class); + } + // ... tests ... +} +``` diff --git a/.agents/project-structure-expectations.md b/.agents/project-structure-expectations.md deleted file mode 100644 index 22a3ab7d61..0000000000 --- a/.agents/project-structure-expectations.md +++ /dev/null @@ -1,21 +0,0 @@ -# ๐Ÿ“ Project structure expectations - -```yaml -.github -buildSrc/ - - src/ - โ”œโ”€โ”€ main/ - โ”‚ โ”œโ”€โ”€ kotlin/ # Kotlin source files - โ”‚ โ””โ”€โ”€ java/ # Legacy Java code - โ”œโ”€โ”€ test/ - โ”‚ โ””โ”€โ”€ kotlin/ # Unit and integration tests - build.gradle.kts # Kotlin-based build configuration - - -build.gradle.kts # Kotlin-based build configuration -settings.gradle.kts # Project structure and settings -README.md # Project overview -AGENTS.md # Entry point for LLM agent instructions -version.gradle.kts # Declares the project version in versioned Gradle Build Tools repos. -``` diff --git a/.agents/project.md b/.agents/project.md deleted file mode 100644 index 9e415d8a45..0000000000 --- a/.agents/project.md +++ /dev/null @@ -1,38 +0,0 @@ -# Project: base-libraries - -## Overview - -`base-libraries` is a foundational JVM repository in the Spine SDK organisation. -It hosts the common data types, annotations, environment helpers, and -parsing/serialization utilities that the rest of the Spine SDK (notably -[`core-jvm`](https://github.com/SpineEventEngine/core-java)) depends on. The -artifacts published from this repo sit at the bottom of the Spine dependency -graph, so changes here ripple into most other Spine projects. - -## Architecture - -Role: **library** (multi-module Gradle build) publishing the following Maven -artifacts under the `io.spine` group: - -- `annotations` โ€” annotation types used across the Spine SDK. -- `base` โ€” common data types and utilities. Not consumed directly by - end users; re-exposed as an `api` dependency by `spine-client` and - `spine-server` in `core-jvm`. -- `environment` โ€” runtime environment detection helpers. -- `format` โ€” parsers for YAML, JSON, binary Protobuf, and Protobuf JSON; - used internally by Spine SDK components. - -Key constraints: - -- Public API stability matters: downstream Spine repos pin to versions - published from here, so removals and signature changes are breaking. -- No analytics, telemetry, reflection, or unsafe code (see - `.agents/safety-rules.md`). -- Versioning follows the Spine SDK policy (`.agents/version-policy.md`); - CI's `Version Guard` rejects branches that reuse a published version. -- Dependency declarations live under - `buildSrc/src/main/kotlin/io/spine/dependency/` and are audited by the - `dependency-audit` skill. - -Read [`.agents/jvm-project.md`](jvm-project.md) for build stack, coding -style, tests, and versioning. diff --git a/.agents/project.md b/.agents/project.md new file mode 120000 index 0000000000..7e0bf9bd38 --- /dev/null +++ b/.agents/project.md @@ -0,0 +1 @@ +../docs/project.md \ No newline at end of file diff --git a/.agents/project.template.md b/.agents/project.template.md deleted file mode 100644 index b6882e03af..0000000000 --- a/.agents/project.template.md +++ /dev/null @@ -1,18 +0,0 @@ - - -# Project: - -## Overview - -*One paragraph: what this repo is, what problem it solves, and its role in the -Spine SDK organisation.* - -## Architecture - -*Role in the org: library / tool / Gradle plugin / application. -Key patterns, public API boundaries, and constraints specific to this repo.* - - diff --git a/.agents/quick-reference-card.md b/.agents/quick-reference-card.md deleted file mode 100644 index 2e890e4289..0000000000 --- a/.agents/quick-reference-card.md +++ /dev/null @@ -1,7 +0,0 @@ -# ๐Ÿ“ Quick Reference Card - -๐Ÿšซ **Do not write to git history** (commit/push/tag/rebase/merge/cherry-pick/reset/`gh pr merge`) without explicit authorization. See -[`safety-rules.md`](safety-rules.md) โ†’ *Commits and history-writing*. -Authorization comes only from a skill's `## Commit authorization` -section or from the user's current prompt โ€” never from prior turns or -memory. diff --git a/.agents/refactoring-guidelines.md b/.agents/refactoring-guidelines.md deleted file mode 100644 index 191db49f5f..0000000000 --- a/.agents/refactoring-guidelines.md +++ /dev/null @@ -1,3 +0,0 @@ -# โš™๏ธ Refactoring guidelines - -- Do NOT replace Kotest assertions with standard Kotlin's built-in test assertions. diff --git a/.agents/running-builds.md b/.agents/running-builds.md deleted file mode 100644 index db0338d6f9..0000000000 --- a/.agents/running-builds.md +++ /dev/null @@ -1,18 +0,0 @@ -# Running builds - -1. When modifying code, run: - ```bash - ./gradlew build - ``` - -2. If Protobuf (`.proto`) files are modified run: - ```bash - ./gradlew clean build - ``` - -3. Documentation-only changes in Kotlin or Java sources run: - ```bash - ./gradlew dokka - ``` - -4. Documentation-only changes do not require running tests! diff --git a/.agents/safety-rules.md b/.agents/safety-rules.md deleted file mode 100644 index e7fece3ccb..0000000000 --- a/.agents/safety-rules.md +++ /dev/null @@ -1,49 +0,0 @@ -# Safety rules - -- โœ… All code must compile and pass static analysis. -- โœ… Do not auto-update external dependencies. -- โŒ Never use reflection or unsafe code without an explicit approval. -- โŒ No analytics or telemetry code. -- โŒ No blocking calls inside coroutines. - -## Commits and history-writing - -**Default: do not write to git history.** This is a hard rule for every -agent โ€” the main thread, every subagent, every skill. It overrides any -local convenience or "the change looks done" instinct. - -The rule covers all of these operations: - -- `git commit`, `git commit-tree` -- `git push`, `git push --force` -- `git tag` -- `git rebase`, `git merge`, `git cherry-pick` against shared history -- `git reset` that discards committed work -- `gh release create`, `gh pr merge` - -Authorization to perform one of these operations exists only when **one** -of the following is true *right now*: - -1. **Skill-declared.** The currently active skill's `SKILL.md` contains - a `## Commit authorization` section that explicitly authorizes the - operation and constrains it (which files may be staged, the exact - commit subject, the maximum number of commits). The mere mention of - a commit message inside skill prose is **not** authorization โ€” the - section heading must be present. -2. **User-instructed.** The user's *current* prompt explicitly tells - the agent to perform the operation. Examples that qualify: - "commit this", "make a commit with subject X", "push the branch", - "tag this release". Authorization from previous turns, from - `CLAUDE.md`, or from any memory file does **not** carry over. - -If neither holds, the agent: - -1. Stages relevant changes with `git add` (only if helpful for review). -2. Prints the proposed commit subject (if any) and `git diff --staged`. -3. **Stops.** The user runs the commit themselves, or replies with - explicit authorization in the next prompt. - -The project's `.claude/settings.json` keeps `Bash(git commit:*)` in -`permissions.ask` as defense-in-depth, but the primary enforcement is -this rule โ€” agents must not propose commit attempts that rely on the -user clicking the prompt. diff --git a/.agents/scripts b/.agents/scripts new file mode 120000 index 0000000000..96bf06e128 --- /dev/null +++ b/.agents/scripts @@ -0,0 +1 @@ +shared/scripts \ No newline at end of file diff --git a/.agents/scripts/api-discovery/.gitignore b/.agents/scripts/api-discovery/.gitignore deleted file mode 100644 index c824ff1d5b..0000000000 --- a/.agents/scripts/api-discovery/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Per-developer override of for the api-discovery -# extraction cache. Contains an absolute path; do not commit. -.workspace-root diff --git a/.agents/scripts/api-discovery/README.md b/.agents/scripts/api-discovery/README.md deleted file mode 100644 index de4c631a11..0000000000 --- a/.agents/scripts/api-discovery/README.md +++ /dev/null @@ -1,158 +0,0 @@ -# `api-discovery` scripts - -Resolve the on-disk location of a Maven artifact's source code for -agents and developers, without repeatedly `unzip`-ing JARs out of the -Gradle cache. - -The agent-facing documentation lives in -[`../../skills/api-discovery/SKILL.md`](../../skills/api-discovery/SKILL.md); -this file is the implementation reference. - -## Why - -Agents investigating library APIs used to run dozens of `find -~/.gradle/caches` + `unzip -l` + `unzip -p` calls per question. Each -`unzip` decompresses the archive from scratch โ€” slow and token-heavy. - -This package replaces that pattern with two cheap reads: - -1. **Sibling-first** โ€” every Spine artifact maps to a sibling clone - under `//`. The source tree is already on - disk; we just resolve the right submodule path. -2. **Extraction cache** โ€” non-Spine artifacts (Jackson, Guava, etc.) - have their `-sources.jar` extracted **once** to a per-workstation - cache. Subsequent queries return instantly. - -## Layout - -``` -.agents/scripts/api-discovery/ -โ”œโ”€โ”€ README.md # this file -โ”œโ”€โ”€ lib/common.sh # shared bash helpers -โ”œโ”€โ”€ discover # main entry โ€” resolve a coordinate to a path -โ”œโ”€โ”€ extract-sources # one-shot JAR extraction (race-safe) -โ”œโ”€โ”€ update-sibling # `git pull --ff-only` a stale sibling (safe-guarded) -โ””โ”€โ”€ clean-cache # prune the extraction cache -``` - -The cache itself is **not** under the repo. It lives at: - -``` -/.agents/caches/api-discovery/sources//// -``` - -`` defaults to the parent of the consumer repo (e.g. -`/Users//Projects/Spine/` when the consumer repo is -`.../Spine/config/`). To override, write the absolute path to an -alternative root into `.workspace-root` next to this README (the -script is gitignored). - -## Bootstrap - -First-time use needs the cache directory created. The scripts detect -its absence and exit `10`; the skill instructs the agent to ask the -user whether to: - -1. **Approve** the default path, -2. **Provide an alternative** workspace root, -3. **Disable** the cache for this repo (recorded in per-developer - auto-memory; sibling-first resolution still works). - -## Scripts - -### `discover` - -``` -discover :: -discover : # version pulled from buildSrc -discover # Spine-only; group + version inferred -``` - -- **stdout** โ€” absolute path to a directory you can `Grep`/`Read`. -- **stderr** โ€” `STALE` warnings when the sibling's `versionToPublish` - differs from the declared dependency version, plus other - diagnostics. Always inspect. -- **exit 0** โ€” path resolved (even if stale; the warning is on stderr). -- **exit 1** โ€” unresolvable (missing sibling AND no sources JAR). -- **exit 10** โ€” cache uninitialized; run the bootstrap flow. - -### `extract-sources` - -``` -extract-sources :: -``` - -Idempotent and race-safe. If the target directory is already populated -the script returns its path immediately. Concurrent first-time -extractions race on an atomic `mv` of a per-PID temp directory; the -loser discards its temp. - -### `update-sibling` - -``` -update-sibling # resolved under -update-sibling # acts on that path directly -``` - -Invoked by the agent (after user consent) when `discover` emits a -`STALE` warning. Safe by design: - -- Pulls **only** when the sibling is on `master` or `main` with a - clean working tree and a tracked upstream. -- On any other branch, exits `0` without touching anything โ€” the - user's "advancing multiple subprojects at once" workflow keeps - feature branches checked out as intentional staging state. -- Refuses on detached HEAD (`3`), uncommitted changes (`4`), or - missing upstream (`5`). -- Never switches branches, never `--rebase`, never `--force`. - -On success (exit `0`), the script writes a single stable token to -**stdout** that names the outcome โ€” callers should branch on the -token, not on stderr text. Failure paths produce empty stdout. - -Exit codes: - -| Code | stdout | Meaning | -|---|---|---| -| `0` | `pulled` | HEAD advanced to upstream tip | -| `0` | `up-to-date` | Already at upstream tip; nothing to do | -| `0` | `skipped-branch` | On a non-default branch; left untouched | -| `1` | _(empty)_ | Sibling not on disk | -| `2` | _(empty)_ | Not a git repository | -| `3` | _(empty)_ | Detached HEAD โ€” refused | -| `4` | _(empty)_ | Working tree dirty โ€” refused | -| `5` | _(empty)_ | No upstream tracking on default branch โ€” refused | -| `6` | _(empty)_ | `git pull --ff-only` failed (divergence, network, etc.) | -| `64` | _(empty)_ | Usage error (no/too many arguments) โ€” BSD `sysexits(3)` convention | - -### `clean-cache` - -``` -clean-cache --dry-run -clean-cache --older-than 30d [--dry-run] -clean-cache --all [--dry-run] -``` - -Manual pruning only. Nothing runs on a timer. - -## Conventions - -- **Bash 3.2 compatible** โ€” macOS ships 3.2 by default. -- **No external dependencies** beyond `bash`, coreutils, `grep`, - `sed`, `awk`, `unzip`, `find`, and `git` (used only by - `update-sibling`). -- **stdout** is always the answer; **stderr** is diagnostics. Mix - them only by piping. -- Scripts source `lib/common.sh` after setting - `SPINE_API_DISCOVERY_DIR`, so the workspace-root pointer file is - reachable. - -## Troubleshooting - -| Symptom | Likely cause | Fix | -|---|---|---| -| `cache not initialized` (exit 10) | Bootstrap not run | Follow the skill's bootstrap prompt | -| `sibling not on disk` | Spine repo not cloned | `git clone` it next to your consumer repo | -| `STALE: ...` | Sibling drifted from declared version | Run `update-sibling ` (auto-skips feature branches), or accept the warning | -| `is in the Gradle cache but publishes no -sources.jar` | Upstream artifact has no sources | Read the binary `.class` files via a different tool, or look at GitHub directly | -| `is not in the local Gradle cache` | Gradle has not fetched the dep | `./gradlew dependencies` to populate, then retry | diff --git a/.agents/scripts/api-discovery/clean-cache b/.agents/scripts/api-discovery/clean-cache deleted file mode 100755 index 30f6049202..0000000000 --- a/.agents/scripts/api-discovery/clean-cache +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env bash -# -# Prune the workstation api-discovery extraction cache. -# -# Usage: -# clean-cache --dry-run # list what would be removed (no deletes) -# clean-cache --older-than 30d # remove entries older than 30 days -# clean-cache --older-than 7d --dry-run -# clean-cache --all # wipe the whole sources cache -# clean-cache --all --dry-run -# -# The cache is at -# `/.agents/caches/api-discovery/sources////`. -# "Age" is the directory's mtime (recorded at extraction time). -# -# Exit codes (see lib/common.sh): -# 0 โ€” succeeded (with or without removals). -# 1 โ€” bad arguments or filesystem error. -# 10 โ€” cache not initialized (nothing to clean). -set -euo pipefail - -SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -SPINE_API_DISCOVERY_DIR="$SCRIPT_DIR" -export SPINE_API_DISCOVERY_DIR -# shellcheck source=lib/common.sh -. "$SCRIPT_DIR/lib/common.sh" - -usage() { - cat >&2 <<'EOF' -Usage: - clean-cache --dry-run - clean-cache --older-than [--dry-run] - clean-cache --all [--dry-run] - -DURATION is a `find -mtime`-style suffix: `7d`, `30d`, `90d` (days only). -EOF - exit "$EX_FAIL" -} - -mode="" -days="" -dry_run=0 - -while [ "$#" -gt 0 ]; do - case "$1" in - --dry-run) - dry_run=1 - shift - ;; - --all) - mode="all" - shift - ;; - --older-than) - mode="older-than" - shift - [ "$#" -gt 0 ] || usage - case "$1" in - *d) days="${1%d}" ;; - *) log_warn "duration must end in 'd' (days), got: $1"; usage ;; - esac - case "$days" in - ''|*[!0-9]*) log_warn "duration days must be numeric, got: $1"; usage ;; - esac - shift - ;; - -h|--help) - usage - ;; - *) - log_warn "unknown argument: $1" - usage - ;; - esac -done - -# Default to a no-op listing if no mode was given. -[ -n "$mode" ] || mode="older-than-default" - -if ! cache_initialized; then - log_warn "cache not initialized: $(cache_root)" - exit "$EX_NO_CACHE" -fi - -sources="$(sources_root)" - -# Collect targets into a temp list so we can preview and act consistently. -list_file="$(mktemp -t api-discovery-clean.XXXXXX)" -trap 'rm -f -- "$list_file"' EXIT - -case "$mode" in - all) - find "$sources" -mindepth 3 -maxdepth 3 -type d -print > "$list_file" 2>/dev/null || true - ;; - older-than) - find "$sources" -mindepth 3 -maxdepth 3 -type d -mtime "+$days" -print \ - > "$list_file" 2>/dev/null || true - ;; - older-than-default) - log_warn "no mode specified; use --all or --older-than d" - usage - ;; -esac - -count="$(wc -l < "$list_file" | tr -d '[:space:]')" - -# Prune now-empty `/` and `//` parents so the cache -# layout stays tidy. Two passes (artifact dirs first, then group dirs) so -# that emptying a group's last artifact also reclaims the group dir. -# Skipped under --dry-run so the command stays read-only. -prune_empty_parents() { - find "$sources" -mindepth 2 -maxdepth 2 -type d -empty -exec rmdir -- {} + \ - 2>/dev/null || true - find "$sources" -mindepth 1 -maxdepth 1 -type d -empty -exec rmdir -- {} + \ - 2>/dev/null || true -} - -if [ "$count" -eq 0 ]; then - log_warn "no entries match; cache untouched" - [ "$dry_run" -eq 0 ] && prune_empty_parents - exit "$EX_OK" -fi - -if [ "$dry_run" -eq 1 ]; then - log_warn "would remove $count entr$( [ "$count" -eq 1 ] && printf 'y' || printf 'ies' ):" - cat -- "$list_file" - exit "$EX_OK" -fi - -removed=0 -while IFS= read -r path; do - [ -n "$path" ] || continue - if rm -rf -- "$path"; then - removed=$((removed + 1)) - else - log_warn "failed to remove: $path" - fi -done < "$list_file" - -prune_empty_parents - -log_warn "removed $removed entr$( [ "$removed" -eq 1 ] && printf 'y' || printf 'ies' )" -exit "$EX_OK" diff --git a/.agents/scripts/api-discovery/discover b/.agents/scripts/api-discovery/discover deleted file mode 100755 index 2b69d36bac..0000000000 --- a/.agents/scripts/api-discovery/discover +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/env bash -# -# Resolve the on-disk location of source code for a Maven artifact, so -# agents can `Grep`/`Read` it directly instead of repeatedly `unzip`ing -# sources JARs out of the Gradle cache. -# -# Strategy: -# 1. Spine artifacts (group = io.spine / io.spine.tools / etc.) are -# served from a sibling clone under `//`. The -# sibling is identified from the `github.com/SpineEventEngine/` -# URL inside the matching `buildSrc/.../dependency/local/.kt` -# file. Submodule paths are resolved by trying canonical name -# transformations (see `resolve_submodule_path`). -# 2. Anything else (and Spine fallbacks when no sibling is on disk) -# is served from the per-workstation extraction cache populated by -# `extract-sources`. -# -# Usage: -# discover :: -# discover : -# discover -# -# Output: -# stdout: absolute path to a directory containing the source tree. -# stderr: freshness warnings and explanatory diagnostics. Always -# inspect stderr before relying on the resolved path. -# -# Exit codes (see lib/common.sh): -# 0 โ€” path resolved (path on stdout). -# 1 โ€” unresolvable (sibling missing AND extraction failed). -# 10 โ€” workstation cache directory not initialized AND the query -# requires the cache (i.e. sibling-first did not succeed). -# Spine-sibling resolution never triggers EX_NO_CACHE โ€” the -# skill's "Non-cached" mode keeps working without bootstrap. -set -euo pipefail - -SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -SPINE_API_DISCOVERY_DIR="$SCRIPT_DIR" -export SPINE_API_DISCOVERY_DIR -# shellcheck source=lib/common.sh -. "$SCRIPT_DIR/lib/common.sh" - -usage() { - cat >&2 <<'EOF' -Usage: discover - -Where is one of: - group:artifact:version e.g. io.spine:spine-base:2.0.0-SNAPSHOT.390 - group:artifact e.g. io.spine:spine-base - artifact e.g. spine-base - -Spine artifacts resolve to the local sibling clone; non-Spine -artifacts resolve to the extracted-sources cache. -EOF - exit "$EX_FAIL" -} - -[ "$#" -eq 1 ] || usage - -parse_query "$1" -group="$Q_GROUP" -artifact="$Q_ARTIFACT" -version="$Q_VERSION" - -if [ -z "$artifact" ]; then - log_warn "empty artifact in query: $1" - exit "$EX_FAIL" -fi - -# NOTE: We intentionally do NOT check `cache_initialized` here. The -# Spine-sibling path doesn't need the cache, and the skill's -# "Non-cached" bootstrap option must keep that path working without -# bootstrap. If we end up falling through to `extract-sources`, that -# script enforces the cache check on its own and raises EX_NO_CACHE. - -# Try the sibling path for Spine artifacts. If the group is empty we -# still attempt the local-deps lookup; a hit means it's a Spine artifact. -try_sibling() { - local dep_file - dep_file="$(find_local_dep_file_for_artifact "$artifact")" - [ -n "$dep_file" ] || return 1 - - local sibling_name workspace sibling_path - sibling_name="$(sibling_name_from_dep_file "$dep_file")" - workspace="$(workspace_root)" || return 1 - sibling_path="$workspace/$sibling_name" - - if [ ! -d "$sibling_path" ]; then - log_warn "sibling not on disk: $sibling_path (declared in $dep_file)" - return 1 - fi - - local module_path - module_path="$(resolve_submodule_path "$sibling_path" "$artifact")" - - if [ ! -d "$module_path" ]; then - log_warn "resolved submodule path missing: $module_path" - return 1 - fi - - # Freshness check: declared version vs sibling's published version. - local declared sibling_v - declared="${version:-$(read_declared_version "$dep_file" || true)}" - sibling_v="$(read_sibling_version "$sibling_path" 2>/dev/null || true)" - - if [ -n "$declared" ] && [ -n "$sibling_v" ] && \ - [ "$declared" != "$sibling_v" ]; then - log_warn "STALE: $artifact declared $declared in $(basename -- "$dep_file") but sibling publishes $sibling_v" - log_warn "sources at $module_path may differ from the published artifact" - fi - - printf '%s\n' "$module_path" - return 0 -} - -# Try sibling first when it could plausibly be a Spine artifact. -if [ -z "$group" ] || is_spine_group "$group"; then - if try_sibling; then - exit "$EX_OK" - fi -fi - -# Fall back to the extraction cache. This needs a full coordinate. -if [ -z "$group" ] || [ -z "$artifact" ] || [ -z "$version" ]; then - # Try to fill in the missing pieces from a local dep file. - if [ -z "$version" ]; then - dep_file="$(find_local_dep_file_for_artifact "$artifact" || true)" - if [ -n "$dep_file" ]; then - version="$(read_declared_version "$dep_file" || true)" - fi - fi - if [ -z "$group" ]; then - # The local Spine objects all use Spine.group / Spine.toolsGroup. - # Without a sibling hit we can't disambiguate; require explicit group. - log_warn "cannot resolve $artifact without a Maven group" - log_warn "retry with :[:]" - exit "$EX_FAIL" - fi - if [ -z "$version" ]; then - log_warn "cannot resolve $group:$artifact without a version" - log_warn "retry with ::" - exit "$EX_FAIL" - fi -fi - -# Delegate to extract-sources. It handles "already cached" by returning -# the path immediately, so this path is fast on repeat queries. -# The `|| exit $?` idiom propagates extract-sources' exit code under -# `set -e`; do NOT split into `target=...; status=$?` โ€” `set -e` may -# terminate the script before the status check runs. -target="$("$SCRIPT_DIR/extract-sources" "$group:$artifact:$version")" || exit $? - -printf '%s\n' "$target" diff --git a/.agents/scripts/api-discovery/extract-sources b/.agents/scripts/api-discovery/extract-sources deleted file mode 100755 index a8456680a7..0000000000 --- a/.agents/scripts/api-discovery/extract-sources +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env bash -# -# Extract a `-sources.jar` from the local Gradle cache into the workstation -# api-discovery cache. Idempotent and race-safe: a second invocation that -# observes an existing target returns immediately; concurrent first-time -# extractions race on the atomic `mv` of a per-PID temp directory. -# -# Usage: -# extract-sources :: -# -# Output: -# stdout: absolute path to the extracted source tree. -# stderr: explanatory diagnostics on cache misses or failures. -# -# Exit codes (see lib/common.sh for the shared definitions): -# 0 โ€” extraction successful (or already cached). -# 1 โ€” sources unavailable (missing JAR, no `-sources` variant, etc.). -# 10 โ€” workstation cache directory not initialized. -# -set -euo pipefail - -SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -SPINE_API_DISCOVERY_DIR="$SCRIPT_DIR" -export SPINE_API_DISCOVERY_DIR -# shellcheck source=lib/common.sh -. "$SCRIPT_DIR/lib/common.sh" - -usage() { - cat >&2 <<'EOF' -Usage: extract-sources :: - -Extracts the matching `-sources.jar` from the local Gradle cache into -`/.agents/caches/api-discovery/sources////`. -EOF - exit "$EX_FAIL" -} - -[ "$#" -eq 1 ] || usage - -parse_query "$1" -group="$Q_GROUP" -artifact="$Q_ARTIFACT" -version="$Q_VERSION" - -if [ -z "$group" ] || [ -z "$artifact" ] || [ -z "$version" ]; then - log_warn "extract-sources requires a full coordinate ::" - exit "$EX_FAIL" -fi - -if ! cache_initialized; then - log_warn "cache not initialized: $(cache_root)" - log_warn "run the api-discovery bootstrap flow to create it" - exit "$EX_NO_CACHE" -fi - -target="$(sources_root)/$group/$artifact/$version" - -if [ -d "$target" ] && [ -n "$(ls -A -- "$target" 2>/dev/null)" ]; then - printf '%s\n' "$target" - exit "$EX_OK" -fi - -sources_jar="$(find_gradle_cache_jar "$group" "$artifact" "$version" -sources)" -if [ -z "$sources_jar" ]; then - if [ -n "$(find_gradle_cache_jar "$group" "$artifact" "$version")" ]; then - log_warn "$group:$artifact:$version is in the Gradle cache but publishes no -sources.jar" - exit "$EX_FAIL" - fi - log_warn "$group:$artifact:$version is not in the local Gradle cache" - log_warn "run './gradlew dependencies' (or rebuild) to fetch it, then retry" - exit "$EX_FAIL" -fi - -parent="$(dirname -- "$target")" -mkdir -p -- "$parent" - -# Race-safe extraction: -# 1) extract into a sibling temp dir whose name embeds our PID, -# 2) check that the final target does not yet exist (race-lost -# detection); if it does, drop our temp and use the existing tree, -# 3) otherwise `mv tmp target`. Note that on macOS/Linux, `mv` into an -# existing directory silently moves the source INSIDE it โ€” we cannot -# rely on `mv` failing when the race is lost. The pre-test catches -# the common case; step 4 catches the narrow window between test -# and mv. -# 4) post-mv, detect the rare race where `target` materialized between -# our existence test and the mv: in that case `target/` -# now exists; remove it. -tmp="${target}.tmp.$$" -trap 'rm -rf -- "$tmp"' EXIT -rm -rf -- "$tmp" -mkdir -p -- "$tmp" - -if ! unzip -q -o -- "$sources_jar" -d "$tmp"; then - log_warn "unzip failed for $sources_jar" - exit "$EX_FAIL" -fi - -if [ -e "$target" ]; then - # Another process beat us to it. Discard our temp; use theirs. - rm -rf -- "$tmp" -else - if ! mv -- "$tmp" "$target"; then - log_warn "could not move extracted sources into $target" - exit "$EX_FAIL" - fi - # Close the narrow race window: if `target` appeared between the - # existence test and the mv, `mv` silently moved `$tmp` INSIDE the - # winning extraction. Clean up the nested debris. - nested="$target/$(basename -- "$tmp")" - if [ -e "$nested" ]; then - rm -rf -- "$nested" - fi -fi -trap - EXIT - -log_warn "extracted $group:$artifact:$version -> $target" -printf '%s\n' "$target" diff --git a/.agents/scripts/api-discovery/lib/common.sh b/.agents/scripts/api-discovery/lib/common.sh deleted file mode 100644 index fa3e9c833e..0000000000 --- a/.agents/scripts/api-discovery/lib/common.sh +++ /dev/null @@ -1,364 +0,0 @@ -#!/usr/bin/env bash -# -# Shared helpers for the api-discovery scripts. -# Sourced by ../discover, ../extract-sources, ../clean-cache. -# -# All functions write diagnostics to stderr; "return values" go to stdout. -# -# Conventions: -# - Bash 3.2 compatible (macOS default). -# - No external deps beyond coreutils, grep, sed, unzip. -# - `set -euo pipefail` is set by the caller, not here. - -# Exit codes used across the scripts. -readonly EX_OK=0 -readonly EX_FAIL=1 -readonly EX_NO_CACHE=10 # cache uninitialized; agent runs bootstrap - -# Resolve the consumer repository root โ€” the nearest ancestor of $PWD -# containing `buildSrc/src/main/kotlin/io/spine/dependency/`. Falls back to -# CLAUDE_PROJECT_DIR (set by Claude Code) if no such ancestor exists. -# Prints the absolute path to stdout. Exits non-zero if it cannot resolve. -consumer_repo_root() { - local dir="${1:-$PWD}" - while [ "$dir" != "/" ] && [ -n "$dir" ]; do - if [ -d "$dir/buildSrc/src/main/kotlin/io/spine/dependency" ]; then - printf '%s\n' "$dir" - return 0 - fi - dir="$(dirname -- "$dir")" - done - if [ -n "${CLAUDE_PROJECT_DIR:-}" ] && \ - [ -d "$CLAUDE_PROJECT_DIR/buildSrc/src/main/kotlin/io/spine/dependency" ]; then - printf '%s\n' "$CLAUDE_PROJECT_DIR" - return 0 - fi - return 1 -} - -# Resolve the workspace root (parent of the consumer repo by default). -# Honors an optional pointer file at -# `/.workspace-root` containing an absolute path; used when the -# user picks "alternative root" during bootstrap. -workspace_root() { - local scripts_dir="${SPINE_API_DISCOVERY_DIR:-}" - if [ -z "$scripts_dir" ]; then - local repo - repo="$(consumer_repo_root)" || return 1 - scripts_dir="$repo/.agents/scripts/api-discovery" - fi - local pointer="$scripts_dir/.workspace-root" - if [ -f "$pointer" ]; then - # Read the first line verbatim. `IFS=` keeps internal spaces - # (paths like `/Users/me/Spine Workspace` must survive intact); - # `read -r` strips the trailing newline. Strip a stray CR for - # Windows-style line endings. - local custom="" - IFS= read -r custom < "$pointer" 2>/dev/null || true - custom="${custom%$'\r'}" - if [ -n "$custom" ] && [ -d "$custom" ]; then - printf '%s\n' "$custom" - return 0 - fi - fi - local repo - repo="$(consumer_repo_root)" || return 1 - (cd "$repo/.." && pwd) -} - -# Directory that holds the per-workstation api-discovery cache. -cache_root() { - local ws - ws="$(workspace_root)" || return 1 - printf '%s/.agents/caches/api-discovery\n' "$ws" -} - -# Subdirectory under cache_root where extracted sources live. -sources_root() { - local root - root="$(cache_root)" || return 1 - printf '%s/sources\n' "$root" -} - -# Returns 0 if the sources cache directory exists; 1 otherwise. -cache_initialized() { - local s - s="$(sources_root)" || return 1 - [ -d "$s" ] -} - -# Returns the first Gradle-cache JAR path matching the coordinates and -# optional suffix ("-sources" or empty). Empty stdout means "not found". -find_gradle_cache_jar() { - local group="$1" artifact="$2" version="$3" suffix="${4:-}" - local base="$HOME/.gradle/caches/modules-2/files-2.1/$group/$artifact/$version" - [ -d "$base" ] || return 0 - local jar - jar="$(find "$base" -maxdepth 2 -type f \ - -name "${artifact}-${version}${suffix}.jar" 2>/dev/null \ - | head -n 1)" - [ -n "$jar" ] && printf '%s\n' "$jar" - return 0 -} - -# Extract the canonical `const val version` value from a Spine local/.kt -# file. Anchors at line start (with optional access modifier) so that -# `const val version` strings inside KDoc, comments, or other quoted text -# do not match. Each local/.kt is expected to declare exactly one -# top-level `version` constant; multi-artifact files use different -# constant names (e.g. `mcVersion`) for their non-canonical versions. -read_declared_version() { - local file="$1" - [ -f "$file" ] || return 1 - sed -nE 's/^[[:space:]]*(private[[:space:]]+|internal[[:space:]]+|public[[:space:]]+|protected[[:space:]]+)?const[[:space:]]+val[[:space:]]+version[[:space:]]*=[[:space:]]*"([^"]+)".*/\2/p' \ - "$file" | head -n 1 -} - -# Read a `val [: Type] by extra("VALUE")` declaration from a file. -# Prints VALUE on stdout; empty if not found. -_read_extra_val() { - local file="$1" name="$2" - sed -nE 's/^[[:space:]]*val[[:space:]]+'"$name"'([[:space:]]*:[[:space:]]*[A-Za-z]+)?[[:space:]]+by[[:space:]]+extra\("([^"]+)"\).*/\2/p' \ - "$file" | head -n 1 -} - -# Read the sibling's "main" version from `/version.gradle.kts`. -# Tries (in order): -# 1. `versionToPublish` โ€” canonical name used by most siblings. -# 2. `Version` โ€” e.g. `mcJavaVersion` -# for sibling `mc-java`, `protoDataVersion` for `ProtoData`. -# Returns non-zero if neither is found; callers treat that as -# "freshness check unavailable". -read_sibling_version() { - local sibling="$1" - local file="$sibling/version.gradle.kts" - [ -f "$file" ] || return 1 - - local v - v="$(_read_extra_val "$file" "versionToPublish")" - if [ -n "$v" ]; then - printf '%s\n' "$v" - return 0 - fi - - local sibling_name camel - sibling_name="$(basename -- "$sibling")" - camel="$(camel_case_lower "$sibling_name")Version" - v="$(_read_extra_val "$file" "$camel")" - if [ -n "$v" ]; then - printf '%s\n' "$v" - return 0 - fi - - return 1 -} - -# Convert a PascalCase name to kebab-case. -# Examples: Base -> base; CoreJvm -> core-jvm; CoreJvmCompiler -> core-jvm-compiler. -kebab_case() { - printf '%s\n' "$1" | sed -E 's/([a-z0-9])([A-Z])/\1-\2/g; s/([A-Z]+)([A-Z][a-z])/\1-\2/g' \ - | tr '[:upper:]' '[:lower:]' -} - -# Convert a kebab-case or PascalCase name to camelCase (first letter lowercase). -# Examples: base-libraries -> baseLibraries; mc-java -> mcJava; -# core-jvm-compiler -> coreJvmCompiler; ProtoData -> protoData. -camel_case_lower() { - local input="$1" - local pascal - pascal="$(printf '%s\n' "$input" | awk -F- '{ - out="" - for (i = 1; i <= NF; i++) { - out = out toupper(substr($i, 1, 1)) substr($i, 2) - } - print out - }')" - local first rest - first="$(printf '%s' "$pascal" | cut -c1 | tr '[:upper:]' '[:lower:]')" - rest="$(printf '%s' "$pascal" | cut -c2-)" - printf '%s%s\n' "$first" "$rest" -} - -# Given a Spine local/.kt file, deduce its sibling repository name. -# Priority: -# 1. `https://github.com/SpineEventEngine/` URL inside the file. -# 2. kebab-case of the file's basename (without `.kt`). -sibling_name_from_dep_file() { - local file="$1" - [ -f "$file" ] || return 1 - local from_url - from_url="$(sed -nE 's|.*github\.com/SpineEventEngine/([A-Za-z0-9._-]+).*|\1|p' \ - "$file" | head -n 1)" - if [ -n "$from_url" ]; then - # Trim any trailing slash or punctuation. - from_url="${from_url%/}" - printf '%s\n' "$from_url" - return 0 - fi - local base - base="$(basename -- "$file" .kt)" - kebab_case "$base" -} - -# Returns 0 if the given directory contains a Kotlin/Java source set. -# Recognizes plain `src/main` and Kotlin Multiplatform names such as -# `src/commonMain`, `src/jvmMain`, `src/jsMain`, `src/nativeMain`. -has_source_set() { - local dir="$1" - [ -d "$dir/src" ] || return 1 - local candidate - for candidate in main commonMain jvmMain jsMain nativeMain; do - [ -d "$dir/src/$candidate" ] && return 0 - done - return 1 -} - -# Resolve a submodule inside a sibling that owns a given artifact. -# Tries candidate subdirectory names in order, returning the first that -# contains a recognizable source set: -# 1. Sibling root (single-module siblings such as `reflect`, `testlib`). -# 2. `/` (artifact name == submodule name). -# 3. `/` -# (`spine-base` -> `base-libraries/base`). -# 4. `/-`-stripped>` -# (`spine-protodata-backend` -> `ProtoData/backend`). -# 5. `/` -# (covers `spine-tool-base` -> `tool-base/tool-base`). -# Falls back to the sibling root when no candidate matches. -resolve_submodule_path() { - local sibling="$1" artifact="$2" - [ -d "$sibling" ] || return 1 - - if has_source_set "$sibling"; then - printf '%s\n' "$sibling" - return 0 - fi - - local sibling_name lower_sibling - sibling_name="$(basename -- "$sibling")" - lower_sibling="$(printf '%s' "$sibling_name" | tr '[:upper:]' '[:lower:]')" - - local candidates=() - candidates+=("$artifact") - - case "$artifact" in - spine-*) candidates+=("${artifact#spine-}") ;; - esac - - case "$artifact" in - spine-${lower_sibling}-*) candidates+=("${artifact#spine-${lower_sibling}-}") ;; - ${lower_sibling}-*) candidates+=("${artifact#${lower_sibling}-}") ;; - esac - - candidates+=("$sibling_name") - - local cand - for cand in "${candidates[@]}"; do - [ -n "$cand" ] || continue - if has_source_set "$sibling/$cand"; then - printf '%s/%s\n' "$sibling" "$cand" - return 0 - fi - done - - printf '%s\n' "$sibling" -} - -# Identify whether a Maven group belongs to the Spine sibling ecosystem. -# Returns 0 (true) for Spine groups, 1 (false) otherwise. -is_spine_group() { - case "$1" in - io.spine|io.spine.tools|io.spine.protodata|io.spine.validation) - return 0 - ;; - *) - return 1 - ;; - esac -} - -# Parse a query into group, artifact, version. Sets the globals -# Q_GROUP, Q_ARTIFACT, Q_VERSION. Some may be empty. -# Accepts: `group:artifact:version`, `group:artifact`, `artifact`, or a -# free-form name that we treat as either an artifact or a sibling label. -# Returns 0 always; the caller decides what an empty field means. -parse_query() { - local q="$1" - Q_GROUP=""; Q_ARTIFACT=""; Q_VERSION="" - local rest - case "$q" in - *:*:*) - Q_GROUP="${q%%:*}" - rest="${q#*:}" - Q_ARTIFACT="${rest%%:*}" - Q_VERSION="${rest#*:}" - ;; - *:*) - Q_GROUP="${q%%:*}" - Q_ARTIFACT="${q#*:}" - ;; - *) - Q_ARTIFACT="$q" - ;; - esac -} - -# Escape every non-alphanumeric character so the result is safe to embed -# in a POSIX ERE pattern. Cheap overkill โ€” Maven artifact names should -# never need most of these, but the caller's input is untrusted. -escape_ere() { - printf '%s' "$1" | sed 's/[^A-Za-z0-9]/\\&/g' -} - -# Locate the consumer repo's local/.kt file that declares a Maven artifact. -# Some local files build artifact coordinates via Kotlin string templates -# (`"$prefix-base"`, `"$group:$prefix-java:$version"`). To match those we -# expand the per-file `prefix` constant before grepping. Other template -# variables resolve to literals already present in the source, so a plain -# grep finds them. -# Returns the path of the first matching file (or empty). -find_local_dep_file_for_artifact() { - local artifact="$1" - local repo - repo="$(consumer_repo_root)" || return 1 - local local_dir="$repo/buildSrc/src/main/kotlin/io/spine/dependency/local" - [ -d "$local_dir" ] || return 0 - - # Validate the artifact name against the Maven convention before - # building a regex from it. Reject anything we cannot guarantee is - # safe; this prevents shell-quoted regex metacharacters in - # caller-supplied input from being interpreted by `grep -E`. - case "$artifact" in - ''|*[!A-Za-z0-9._-]*) - log_warn "invalid artifact name (allowed: A-Z a-z 0-9 . _ -): $artifact" - return 1 - ;; - esac - local artifact_esc - artifact_esc="$(escape_ere "$artifact")" - - local file prefix expanded - for file in "$local_dir"/*.kt; do - [ -f "$file" ] || continue - prefix="$(sed -nE 's/.*const[[:space:]]+val[[:space:]]+prefix[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' \ - "$file" | head -1)" - if [ -n "$prefix" ]; then - expanded="$(sed -e 's|\$prefix|'"$prefix"'|g; s|\${prefix}|'"$prefix"'|g' "$file")" - else - expanded="$(cat -- "$file")" - fi - # Match the artifact as a complete coordinate component: - # delimited by `"`, `:`, or `-` on either side, never as a substring. - if printf '%s\n' "$expanded" | grep -qE "[\":-]${artifact_esc}[\":-]|[\":-]${artifact_esc}\$|^${artifact_esc}\$"; then - printf '%s\n' "$file" - return 0 - fi - done - return 0 -} - -# Emit a stderr line tagged with the scripts' identity, so the agent can -# distinguish them from unrelated noise. -log_warn() { - printf 'api-discovery: %s\n' "$*" >&2 -} diff --git a/.agents/scripts/api-discovery/update-sibling b/.agents/scripts/api-discovery/update-sibling deleted file mode 100755 index e145aaee6a..0000000000 --- a/.agents/scripts/api-discovery/update-sibling +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env bash -# -# Refresh a Spine sibling repo on disk so api-discovery returns sources -# matching the most recent published version. -# -# Safe by design: -# - Pulls only when the sibling is on its default branch (master or -# main) with a clean working tree and a tracked upstream. -# - On any other branch, treats the local state as intentional (the -# user is "advancing multiple subprojects at the same time") and -# exits 0 without touching anything. -# - Refuses on detached HEAD, uncommitted changes, or missing upstream. -# - Never switches branches, never `--rebase`, never `--force`, never -# fetches a branch the user does not already track. The strictest -# action it performs is `git pull --ff-only`. -# -# This script is intended to be invoked by the api-discovery skill -# after the agent has surfaced a STALE warning to the user AND received -# explicit consent. -# -# Usage: -# update-sibling # resolved under -# update-sibling # acts on that path directly -# -# Output: -# stdout: exactly one stable token on success (`pulled`, `up-to-date`, -# or `skipped-branch` โ€” see Exit codes). Empty on any failure -# path so callers cannot misread an error as a result. -# stderr: human-facing diagnostics, including git's own pull output. -# -# Exit codes: -# 0 โ€” succeeded; stdout token names the outcome: -# `pulled` โ€” HEAD advanced to upstream tip. -# `up-to-date` โ€” already at upstream tip; nothing to do. -# `skipped-branch` โ€” on a non-default branch; left untouched. -# 1 โ€” sibling not on disk. -# 2 โ€” not a git repository. -# 3 โ€” detached HEAD; refused. -# 4 โ€” uncommitted tracked changes; refused. -# 5 โ€” default branch has no upstream tracking; refused. -# 6 โ€” `git pull --ff-only` failed (divergence, conflict, network, etc.). -# 64 โ€” usage error (no/too many args). -set -euo pipefail - -SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -SPINE_API_DISCOVERY_DIR="$SCRIPT_DIR" -export SPINE_API_DISCOVERY_DIR -# shellcheck source=lib/common.sh -. "$SCRIPT_DIR/lib/common.sh" - -# Layered on top of the shared EX_OK / EX_FAIL / EX_NO_CACHE in common.sh. -readonly EX_NOT_GIT=2 -readonly EX_DETACHED=3 -readonly EX_DIRTY=4 -readonly EX_NO_UPSTREAM=5 -readonly EX_PULL_FAILED=6 -readonly EX_USAGE=64 # BSD sysexits(3) convention. - -usage() { - cat >&2 <<'EOF' -Usage: update-sibling - -Examples: - update-sibling base-libraries - update-sibling /Users/me/Projects/Spine/validation - -Pulls only when the sibling is on its default branch (master|main) -with a clean working tree and a tracked upstream. Otherwise it leaves -the sibling alone. - -On success, prints one of: pulled | up-to-date | skipped-branch. -EOF - exit "$EX_USAGE" -} - -[ "$#" -eq 1 ] || usage -arg="$1" - -# Resolve to an absolute sibling path. Accept either a bare repo name -# (looked up under ) or an absolute path. -case "$arg" in - /*) sibling="$arg" ;; - *) - ws="$(workspace_root)" || { - log_warn "cannot resolve workspace root" - exit "$EX_FAIL" - } - sibling="$ws/$arg" - ;; -esac - -if [ ! -d "$sibling" ]; then - log_warn "sibling not on disk: $sibling" - exit "$EX_FAIL" # distinct from EX_USAGE so callers can tell - # "you passed me a bad path" apart from - # "you didn't pass me anything". -fi - -# `.git` may be a directory (normal clone) or a file (worktree pointer). -if [ ! -e "$sibling/.git" ]; then - log_warn "not a git repository: $sibling" - exit "$EX_NOT_GIT" -fi - -branch="$(git -C "$sibling" rev-parse --abbrev-ref HEAD 2>/dev/null || true)" -if [ -z "$branch" ]; then - log_warn "failed to read current branch in $sibling" - exit "$EX_FAIL" -fi -if [ "$branch" = "HEAD" ]; then - log_warn "$sibling is in detached HEAD; not pulling" - exit "$EX_DETACHED" -fi - -# Non-default branch: the user is intentionally on a feature branch. -# Use the current code as-is. -case "$branch" in - master|main) - ;; - *) - log_warn "$sibling is on '$branch' (not master/main); using local code as-is" - printf 'skipped-branch\n' - exit "$EX_OK" - ;; -esac - -# Dirty-tree guard: refuse on TRACKED modifications, since a fast-forward -# could conflict with them. Untracked files are tolerated: they don't -# conflict with HEAD movement on their own, and any genuine overwrite -# conflict (upstream adds a file whose path already exists untracked -# locally) still surfaces below as EX_PULL_FAILED via git's own check. -if [ -n "$(git -C "$sibling" status --porcelain --untracked-files=no 2>/dev/null)" ]; then - log_warn "$sibling has uncommitted tracked changes on '$branch'; not pulling" - exit "$EX_DIRTY" -fi - -# Upstream guard: --ff-only against an undefined upstream is meaningless. -if ! git -C "$sibling" rev-parse --abbrev-ref --symbolic-full-name '@{u}' \ - >/dev/null 2>&1; then - log_warn "$sibling/$branch has no upstream tracking; not pulling" - exit "$EX_NO_UPSTREAM" -fi - -# Capture HEAD before/after so we can report what changed. -before="$(git -C "$sibling" rev-parse HEAD)" -if ! git -C "$sibling" pull --ff-only >&2; then - log_warn "git pull --ff-only failed in $sibling" - exit "$EX_PULL_FAILED" -fi -after="$(git -C "$sibling" rev-parse HEAD)" - -if [ "$before" = "$after" ]; then - log_warn "$sibling already up-to-date on '$branch' ($after)" - printf 'up-to-date\n' -else - log_warn "$sibling pulled '$branch': $before -> $after" - printf 'pulled\n' -fi -exit "$EX_OK" diff --git a/.agents/scripts/pre-pr-gate.sh b/.agents/scripts/pre-pr-gate.sh deleted file mode 100755 index 3ba36c0e59..0000000000 --- a/.agents/scripts/pre-pr-gate.sh +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env bash -# -# PreToolUse hook: block `gh pr create` unless /pre-pr has successfully run -# for the current HEAD. The hook is intentionally unaware of the repository's -# versioning or build system; the /pre-pr skill decides which checks apply. -# -# Input: hook JSON on stdin (tool_name, tool_input.command). -# Exit: 0 to allow, 2 to block (stderr is surfaced to Claude). -# -set -eu - -if ! command -v jq >/dev/null 2>&1; then - cat >&2 </dev/null) || exit 0 -sentinel="$repo_root/.git/pre-pr.ok" - -block() { - cat >&2 - exit 2 -} - -if [ ! -f "$sentinel" ]; then - block </dev/null 2>&1; then - cat >&2 <<'EOF' -This hook requires `jq` to validate edits to version.gradle.kts and cannot run safely without it. - -Install `jq` and retry. This hook fails closed to avoid silently allowing prohibited edits. -EOF - exit 2 -fi - -input=$(cat) -file=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty') -command=$(printf '%s' "$input" | jq -r '.tool_input.command // empty') - -touches_version_file() { - if [ "$file" = "version.gradle.kts" ] || [ "${file%/version.gradle.kts}" != "$file" ]; then - return 0 - fi - - printf '%s\n' "$command" \ - | grep -qE '^\*\*\* (Add|Update|Delete) File: (.+/)?version\.gradle\.kts$' -} - -if touches_version_file; then - cat >&2 <<'EOF' -Direct edits to version.gradle.kts are blocked by a project hook. - -If this repository already has a root version.gradle.kts, use the bump-version -skill instead: - /bump-version [snapshot|minor|major] - -If this repository does not have a root version.gradle.kts, do not add one just -to satisfy /pre-pr; the version check is not applicable. - -See: - - .agents/version-policy.md - - .agents/skills/bump-version/SKILL.md -EOF - exit 2 -fi - -exit 0 diff --git a/.agents/scripts/publish-version-gate.sh b/.agents/scripts/publish-version-gate.sh deleted file mode 100755 index 996bd25656..0000000000 --- a/.agents/scripts/publish-version-gate.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env bash -# -# PreToolUse hook: block any `./gradlew` invocation that could publish to -# Maven Local without a version bump on the current branch. Wraps the -# Layer-1 deterministic check at `version-bumped.sh`. -# -# This is intentionally broad: it fires on `build`, `publish`, -# `publishToMavenLocal`, and any `:publish*` task. Many repos in this -# constellation chain `publishToMavenLocal` into `build` because -# integration tests consume those local artifacts, so `build` itself is -# publish-risky. False positives (blocking a pure compile) are preferable -# to overwriting a previously published snapshot that consuming repos -# rely on. -# -# Input: hook JSON on stdin (tool_name, tool_input.command). -# Exit: 0 to allow, 2 to block (stderr is surfaced to Claude). -# -set -eu - -command -v jq >/dev/null 2>&1 || exit 0 - -input=$(cat) -tool=$(printf '%s' "$input" | jq -r '.tool_name // empty') -[ "$tool" != "Bash" ] && exit 0 - -cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty') - -# Split the command on shell separators (`;`, `&`, `|`) and inspect each -# segment. Only block when a segment, after optional whitespace, invokes -# `./gradlew` (or `./config/gradlew`) with a publish-risky task. Avoids -# false positives on `echo "./gradlew build"` or fixtures. -risky_segment() { - local seg="$1" - # Must start with a gradlew invocation. - printf '%s' "$seg" | grep -qE '^[[:space:]]*\.?/?(config/)?gradlew([[:space:]]|$)' || return 1 - # Must mention a publish-risky task. `build` is risky because it can - # finalize publishToMavenLocal in this config. The leading - # `(:[A-Za-z0-9_.-]+)*:?` covers qualified task paths - # (e.g. `:module:build`, `:a:b:publishToMavenLocal`) and a single - # leading-colon form (`:publishMavenJavaPublicationToMavenLocal`). - # `publish[^[:space:]]*` then catches every publish-task variant. - printf '%s' "$seg" | grep -qE '(^|[[:space:]])(:[A-Za-z0-9_.-]+)*:?(build|publish[^[:space:]]*|publishToMavenLocal|publishAllPublicationsToMavenLocal)([[:space:]]|$)' -} - -block_needed=0 -# `|| [ -n "$segment" ]` makes the loop process the final segment when the -# input has no trailing newline (which is the case for `printf '%s'`). -while IFS= read -r segment || [ -n "$segment" ]; do - if risky_segment "$segment"; then - block_needed=1 - break - fi -done < <(printf '%s' "$cmd" | tr ';&|' '\n\n\n') - -[ "$block_needed" -eq 0 ] && exit 0 - -repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0 -script="$repo_root/.agents/skills/version-bumped/scripts/version-bumped.sh" - -# If the helper is missing (e.g. partial clone), don't pretend we gated. -if [ ! -x "$script" ]; then - exit 0 -fi - -# `&& rc=0 || rc=$?` captures the exit code regardless of success/failure. -# After `if cmd; then ... fi`, $? reflects the if-fi structural exit (0), -# not the failed test's exit code โ€” so we cannot use the if-fi form here. -err_file="/tmp/version-bumped.$$.err" -VERSION_BUMPED_QUIET=1 "$script" 2>"$err_file" && rc=0 || rc=$? -if [ "$rc" -eq 0 ]; then - rm -f "$err_file" - exit 0 -fi -err_payload=$(cat "$err_file" 2>/dev/null || true) -rm -f "$err_file" - -# Layer-1 returned a configuration error โ€” do not block, surface the note. -if [ "$rc" -ne 1 ]; then - printf '%s\n' "$err_payload" >&2 - exit 0 -fi - -cat >&2 </dev/null 2>&1 || exit 0 - -input=$(cat) -file=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty') -command=$(printf '%s' "$input" | jq -r '.tool_input.command // empty') - -sanitize_file() { - local path="$1" - - [ -z "$path" ] && return 0 - [ ! -f "$path" ] && return 0 - - case "$path" in - *.java|*.kt|*.kts) ;; - *) return 0 ;; - esac - - tmp=$(mktemp) - awk ' - { sub(/[ \t]+$/, "") } - /^$/ { blank++; if (blank > 1) next; print; next } - { blank = 0; print } - ' "$path" > "$tmp" && mv "$tmp" "$path" -} - -if [ -n "$file" ]; then - sanitize_file "$file" - exit 0 -fi - -printf '%s\n' "$command" \ - | sed -nE 's/^\*\*\* (Add|Update) File: (.*)$/\2/p' \ - | sort -u \ - | while IFS= read -r path; do - sanitize_file "$path" - done diff --git a/.agents/scripts/update-copyright.sh b/.agents/scripts/update-copyright.sh deleted file mode 100755 index b25282fda6..0000000000 --- a/.agents/scripts/update-copyright.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env bash -# -# PostToolUse hook: refresh the copyright header of source files touched by -# Edit/Write/MultiEdit. Delegates to -# .agents/skills/update-copyright/scripts/update_copyright.py, which: -# - operates only on recognized source extensions, -# - never adds a header to a file that does not already have one, -# - rewrites `today.year` to the current year per the IntelliJ profile. -# -# Input: hook JSON on stdin. Claude Code passes `tool_input.file_path`; -# Codex `apply_patch` passes the patch text in `tool_input.command`. -# Exit: 0 always (post-tool-use; never block). -# -set -u - -# Required tools โ€” silently no-op if either is missing so the hook never blocks. -command -v jq >/dev/null 2>&1 || exit 0 -command -v python3 >/dev/null 2>&1 || exit 0 - -input=$(cat) -file=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty' 2>/dev/null || true) -command=$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || true) - -root="${CLAUDE_PROJECT_DIR:-$(pwd)}" -script="$root/.agents/skills/update-copyright/scripts/update_copyright.py" - -[ -f "$script" ] || exit 0 - -update_path() { - local path="$1" - [ -z "$path" ] && return 0 - [ ! -f "$path" ] && return 0 - python3 "$script" --root "$root" "$path" >/dev/null 2>&1 || true -} - -if [ -n "$file" ]; then - update_path "$file" - exit 0 -fi - -printf '%s\n' "$command" \ - | sed -nE 's/^\*\*\* (Add|Update) File: (.*)$/\2/p' \ - | sort -u \ - | while IFS= read -r path; do - update_path "$path" - done - -exit 0 diff --git a/.agents/shared b/.agents/shared new file mode 160000 index 0000000000..22a801aa02 --- /dev/null +++ b/.agents/shared @@ -0,0 +1 @@ +Subproject commit 22a801aa02f133db5d264375e3c35241a97638b7 diff --git a/.agents/skills b/.agents/skills new file mode 120000 index 0000000000..f14734dde4 --- /dev/null +++ b/.agents/skills @@ -0,0 +1 @@ +shared/skills \ No newline at end of file diff --git a/.agents/skills/api-discovery/SKILL.md b/.agents/skills/api-discovery/SKILL.md deleted file mode 100644 index b1622ffd10..0000000000 --- a/.agents/skills/api-discovery/SKILL.md +++ /dev/null @@ -1,288 +0,0 @@ ---- -name: api-discovery -description: > - Resolve the on-disk location of a Maven artifact's source code, - so you can `Grep`/`Read` it directly instead of running `unzip` - against JARs in the Gradle cache. Use this whenever you need to - inspect a library's API or implementation โ€” definitions of public - types, method signatures, KDoc, internal helpers, etc. ---- - -# API discovery - -Before reading library source code, run the `discover` script in -`.agents/scripts/api-discovery/`. It returns a path you can hand -straight to `Grep`, `Read`, or `Glob`. - -Do **not** run `find ~/.gradle/caches` or `unzip` against cache JARs. -Each `unzip` decompresses the archive afresh โ€” slow and token-heavy. - -## How to call it - -From the consumer repository root: - -```bash -.agents/scripts/api-discovery/discover -``` - -Where `` is one of: - -| Form | Example | Notes | -|---|---|---| -| `group:artifact:version` | `io.spine:spine-base:2.0.0-SNAPSHOT.390` | Most explicit | -| `group:artifact` | `io.spine:spine-base` | Version inferred from `buildSrc` | -| `artifact` | `spine-base` | Spine-only; group inferred from `buildSrc` | - -The script writes the absolute resolved path to **stdout**, and any -freshness/diagnostic warnings to **stderr**. Always read stderr โ€” a -silent stdout means clean resolution; a noisy stderr means caveats -the user should know about. - -## Exit codes - -| Code | Meaning | What you do | -|---|---|---| -| `0` | Path on stdout is usable. | Pass it to `Grep`/`Read`/`Glob`. If stderr is non-empty, surface the warning to the user before relying on the path. | -| `1` | Unresolvable (no sibling AND no JAR). | Report the failure. **Do not** fall back to `unzip ~/.gradle/caches/...`. | -| `10` | Cache directory not initialized. | Run the **bootstrap flow** below. | - -## Bootstrap flow (exit 10) - -On the first run in a fresh workstation the per-workstation cache -directory does not yet exist. The script exits `10` and names the -path it would create. Ask the user: - -> The shared cache directory `/.agents/caches/api-discovery/` -> does not exist yet. How would you like to proceed? -> -> 1. **Approve** โ€” create the directory at the default path. -> 2. **Alternative root** โ€” pick a different parent for the shared -> `.agents/` directory (e.g., `~/SpineWorkspace`, `/srv/spine`). -> 3. **Non-cached** โ€” skip the extraction cache. Sibling-first -> discovery still works for Spine artifacts; non-Spine deps will -> not be served by `api-discovery` in this repo. - -Then act on the user's reply: - -- **Approve** โ†’ `mkdir -p /.agents/caches/api-discovery/sources`, - then re-run the original `discover` query. -- **Alternative root** โ†’ ask for the absolute path ``, then: - ```bash - mkdir -p "/.agents/caches/api-discovery/sources" - printf '%s\n' "" \ - > .agents/scripts/api-discovery/.workspace-root - ``` - (the pointer file is gitignored). Then re-run `discover`. -- **Non-cached** โ†’ record the choice in **per-developer auto-memory** - (project memory, type `feedback`), `name: api-discovery-cache-disabled`, - describing the user's choice and giving the "How to apply" rule: - *do not invoke `extract-sources` in this repo; for non-Spine deps - fall back to other investigation tools*. Then proceed with - sibling-first only. - -Check that memory at session start. If it exists, skip cache-touching -paths entirely. - -## Workflow - -1. **Always** call `discover` before reading library source. -2. Use the returned path with `Grep`/`Read`/`Glob` directly. Do **not** - `cd` into the directory โ€” that adds path-prefix noise to tool calls - and makes line citations harder to read. -3. If stderr contains `STALE: ...`, the sibling on disk does not match - the version declared in `buildSrc`. Surface the warning AND offer - to refresh โ€” see *Refreshing a stale sibling* below. -4. If the script exits `1`, report the failure with its stderr - message and stop. Do not try `unzip` as a workaround. - -## Refreshing a stale sibling - -The user keeps siblings cloned locally as the source of truth and -sometimes works across several siblings at once with a feature branch -checked out in each. So a `STALE` line has two possible meanings, and -they require different handling: - -- **Sibling is behind `master`/`main`.** A `git pull --ff-only` will - bring it up to date. -- **Sibling is on a feature branch.** This is *intentional* โ€” the user - is staging changes across multiple subprojects. The local code is - the right code; **do not** pull. - -You cannot tell which case applies without inspecting the sibling. The -companion script `update-sibling` handles both safely: it pulls only -on the default branch with a clean tree and a tracked upstream, and -exits `0` without touching anything when on a feature branch. - -### Procedure - -When you see a `STALE: ...` line from `discover`: - -1. Surface the warning to the user. -2. Ask, in one short prompt: - > The sibling at `` is stale. Want me to try updating it? - > I'll only `git pull --ff-only` if it's on `master`/`main` with - > a clean working tree; if you have a feature branch checked out, - > I'll leave it as-is. -3. If the user agrees, run: - ```bash - .agents/scripts/api-discovery/update-sibling - ``` - `` is either the absolute path shown by - `discover` (preferred โ€” unambiguous) or just the sibling repo name - (resolved under ``). -4. Read **stdout** to decide what to do next โ€” it is a single stable - token, not free-form English: - - `pulled` โ€” HEAD advanced. Re-run `discover` so the STALE warning - clears (or, more rarely, reports a different discrepancy). - - `up-to-date` โ€” sibling was already at upstream tip. The STALE - warning is informational โ€” the declared `buildSrc` version and - the sibling's `versionToPublish` simply disagree. Proceed - without re-running `discover`. - - `skipped-branch` โ€” sibling is on a feature branch and was left - untouched. Use the local code as-is; proceed without re-running. - - stderr always carries the human-readable diagnostics; surface it - to the user, but do not parse it to drive control flow. -5. If the user declines, proceed without pulling. Do not ask again - for the same sibling in the same session unless the user revisits. - -### `update-sibling` exit codes - -Exit 0 is split into three outcomes by the **stdout token** โ€” read -that, not the stderr text. - -| Code | stdout | Meaning | What you do | -|---|---|---|---| -| `0` | `pulled` | HEAD advanced to upstream tip. | Re-run `discover` so the STALE warning clears. | -| `0` | `up-to-date` | Already at upstream tip; nothing to do. | Proceed; surface the STALE warning to the user as informational. | -| `0` | `skipped-branch` | On a non-default branch; left untouched. | Use the local code as-is; proceed without re-running. | -| `1` | _(empty)_ | Sibling not on disk. | Report the error. | -| `2` | _(empty)_ | Not a git repository. | Report the error; do not retry. | -| `3` | _(empty)_ | Detached HEAD โ€” refused. | Tell the user; do not retry. | -| `4` | _(empty)_ | Working tree dirty โ€” refused. | Tell the user; do not retry. | -| `5` | _(empty)_ | No upstream tracking on default branch โ€” refused. | Tell the user. | -| `6` | _(empty)_ | `git pull --ff-only` failed (divergence, network, etc.). | Surface the git error verbatim. | -| `64` | _(empty)_ | Usage error (no/too many arguments). | Fix the invocation; do not retry blindly. | - -Failure paths produce **empty stdout** so the agent can never misread -an error message as a result token. - -### "Don't ask me again" - -If the user says something like "stop offering" or "skip the prompt -this session", remember that for the rest of the conversation and do -not prompt on subsequent STALE warnings โ€” just surface the warning -and move on. This is **per-session** state; do not write it to -auto-memory. - -## Anti-patterns - -Stop doing these โ€” they are exactly what this skill exists to replace: - -- `find ~/.gradle/caches/modules-2/files-2.1/ -name '*-sources.jar'` -- `unzip -l ` to list classes -- `unzip -p path/in/jar` to read a file -- Any chain of `unzip` + `grep` against a Gradle-cache JAR - -If you find yourself wanting to do those, run `discover` instead. - -## Examples - -**Spine artifact, fresh sibling on disk:** - -```text -$ .agents/scripts/api-discovery/discover io.spine:spine-base -/Users//Projects/Spine/base-libraries/base -$ echo $? -0 -``` - -Tool calls then look like: - -- `Glob` pattern `**/*.kt`, path - `/Users//Projects/Spine/base-libraries/base`. -- `Grep` pattern `class Identifier`, path the same. - -**Spine artifact, stale sibling:** - -```text -$ .agents/scripts/api-discovery/discover io.spine.tools:validation-java -api-discovery: STALE: validation-java declared 2.0.0-SNAPSHOT.433 in Validation.kt but sibling publishes 2.0.0-SNAPSHOT.440 -api-discovery: sources at /Users//Projects/Spine/validation/java may differ from the published artifact -/Users//Projects/Spine/validation/java -``` - -Surface the `STALE` line, then offer to refresh โ€” see *Refreshing a -stale sibling*. After the user agrees and the pull succeeds, re-run -`discover` and the warning clears. - -**Stale sibling, refresh on master:** - -```text -$ .agents/scripts/api-discovery/update-sibling /Users//Projects/Spine/validation -Updating abc1234..def5678 -Fast-forward - ... -api-discovery: /Users//Projects/Spine/validation pulled 'master': abc1234... -> def5678... -pulled -$ echo $? -0 -``` - -Stdout is `pulled` โ€” re-run `discover` to clear the STALE warning. - -**Stale sibling, already at upstream tip:** - -```text -$ .agents/scripts/api-discovery/update-sibling /Users//Projects/Spine/validation -Already up to date. -api-discovery: /Users//Projects/Spine/validation already up-to-date on 'master' (def5678...) -up-to-date -$ echo $? -0 -``` - -Stdout is `up-to-date` โ€” the sibling is fresh; the STALE warning -reflects a declared-version vs. `versionToPublish` discrepancy that -`git pull` cannot resolve. Surface it to the user as informational. - -**Stale sibling, feature branch (no-op):** - -```text -$ .agents/scripts/api-discovery/update-sibling /Users//Projects/Spine/validation -api-discovery: /Users//Projects/Spine/validation is on 'feature/new-rule' (not master/main); using local code as-is -skipped-branch -$ echo $? -0 -``` - -Stdout is `skipped-branch` โ€” feature branch is intentional local -state. Use the code as-is. - -**Non-Spine artifact, first use (extraction):** - -```text -$ .agents/scripts/api-discovery/discover com.google.guava:guava:33.5.0-jre -api-discovery: extracted com.google.guava:guava:33.5.0-jre -> .../guava/33.5.0-jre -/Users//Projects/Spine/.agents/caches/api-discovery/sources/com.google.guava/guava/33.5.0-jre -``` - -Second call returns the same path with no stderr (already cached). - -**Unresolvable:** - -```text -$ .agents/scripts/api-discovery/discover io.spine:does-not-exist:9.9.9 -api-discovery: io.spine:does-not-exist:9.9.9 is not in the local Gradle cache -api-discovery: run './gradlew dependencies' (or rebuild) to fetch it, then retry -$ echo $? -1 -``` - -Report the failure verbatim; do not try `unzip` as a workaround. - -## Related - -- Implementation reference: `.agents/scripts/api-discovery/README.md`. -- Sibling refresh on STALE: `.agents/scripts/api-discovery/update-sibling`. -- Manual cache pruning: `.agents/scripts/api-discovery/clean-cache`. diff --git a/.agents/skills/bump-gradle/SKILL.md b/.agents/skills/bump-gradle/SKILL.md deleted file mode 100644 index 22f295786d..0000000000 --- a/.agents/skills/bump-gradle/SKILL.md +++ /dev/null @@ -1,148 +0,0 @@ ---- -name: bump-gradle -description: > - Update the Gradle wrapper version used by this repository. Use when asked to - upgrade Gradle, bump the Gradle wrapper, move the project to the latest - Gradle release from the official release notes, run the Gradle build, and - commit Gradle wrapper and dependency report changes separately. ---- - -# Bump Gradle - -Use the official Gradle release notes as the source of truth for both the -latest version and the wrapper update command: - -https://docs.gradle.org/current/release-notes.html#upgrade-instructions - -Always check that page at task time. Do not rely on remembered Gradle versions. - -## Commit authorization - -This skill is authorized to run `git commit` **up to two times** per -invocation, under these constraints: - -1. **Gradle wrapper commit.** Stage only the Gradle wrapper files - (`gradle/wrapper/gradle-wrapper.properties`, - `gradle/wrapper/gradle-wrapper.jar`, `gradlew`, `gradlew.bat`, plus - files directly required by the wrapper update). Subject: - `` Bump Gradle -> `GRADLE_VERSION` `` with the actual version - substituted. Skip if no wrapper-owned file changed. - -2. **Dependency-report commit** (separate from the wrapper commit). Stage - only generated dependency-report files (`docs/dependencies/pom.xml`, - `docs/dependencies/dependencies.md`). Subject: - `Update dependency reports`. Skip if the build did not regenerate - those files. - -No `git push`, `git tag`, `git rebase`, `git commit --amend`, or any other -history-writing operation. Those require a separate authorization -(`.agents/safety-rules.md` โ†’ *Commits and history-writing*). Do not create -empty commits, and do not bundle unrelated changes into either commit. - -## Checklist - -1. Work from the target repository root. - - Confirm `./gradlew` and `gradle/wrapper/gradle-wrapper.properties` exist - before changing anything. Inspect `git status --short` and preserve unrelated - user changes. If Gradle wrapper files are already modified, inspect the diff - and continue only when those edits are part of the same requested Gradle - bump; otherwise ask before overwriting or staging them. - -2. Read the latest Gradle version from the release notes. - - Open the Upgrade instructions section at the URL above. Use the version in - the release heading and the wrapper command shown there. They should agree; - if they do not, stop and report the mismatch. - -3. Run the wrapper update command. - - Substitute the version from the release notes: - - ```bash - ./gradlew wrapper --gradle-version=GRADLE_VERSION && ./gradlew wrapper - ``` - - For example, if the release notes say Gradle `9.5.1`, run: - - ```bash - ./gradlew wrapper --gradle-version=9.5.1 && ./gradlew wrapper - ``` - -4. Run the build. - - ```bash - ./gradlew clean build - ``` - - If the wrapper update or build fails, do not commit partial changes. Report - the failing command and the relevant error output. - -5. Commit only Gradle-related files. - - Inspect `git status --short` and `git diff --name-only`. Stage only files - created or updated by the Gradle wrapper bump, normally: - - ```text - gradle/wrapper/gradle-wrapper.properties - gradle/wrapper/gradle-wrapper.jar - gradlew - gradlew.bat - ``` - - Include other Gradle-owned files only when they are directly required by the - wrapper update and are clearly part of the same change. Do not stage - dependency reports or unrelated build output in this commit. - - Commit with the exact subject, replacing `GRADLE_VERSION`: - - ```text - Bump Gradle -> `GRADLE_VERSION` - ``` - - Example: - - ```bash - git commit -m 'Bump Gradle -> `9.5.1`' - ``` - - If no Gradle-related files changed, do not create an empty commit; report - that the wrapper was already current after verification. - -6. Commit dependency reports separately when the build updates them. - - Stage only generated dependency report files. In repositories using this - config, the usual paths are: - - ```text - docs/dependencies/pom.xml - docs/dependencies/dependencies.md - ``` - - Include other changed files only when they are clearly generated dependency - reports from the build. Commit them separately with: - - ```text - Update dependency reports - ``` - -7. Verify the final branch state. - - Confirm the recent commit subjects and make sure no owned Gradle bump or - dependency report changes remain unstaged: - - ```bash - git log --format=%s -2 - git status --short - ``` - - Leave unrelated pre-existing user changes alone and mention them separately - in the final response. - -8. Ensure `version.gradle.kts` is bumped. - - Before this branch can be built or published locally, the project - version must be strictly greater than the version on the base ref. - Invoke `/version-bumped` โ€” it is a no-op if a bump has already - happened earlier on the branch, and otherwise calls `/bump-version` - to perform the increment. diff --git a/.agents/skills/bump-gradle/agents/openai.yaml b/.agents/skills/bump-gradle/agents/openai.yaml deleted file mode 100644 index 6edf97877f..0000000000 --- a/.agents/skills/bump-gradle/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Bump Gradle" - short_description: "Update the Gradle wrapper safely" - default_prompt: "Use $bump-gradle to update this repository to the latest Gradle wrapper version from the official release notes, build, and split Gradle/report commits." diff --git a/.agents/skills/bump-version/SKILL.md b/.agents/skills/bump-version/SKILL.md deleted file mode 100644 index 8a882be885..0000000000 --- a/.agents/skills/bump-version/SKILL.md +++ /dev/null @@ -1,143 +0,0 @@ ---- -name: bump-version -description: > - Bump the project version in `version.gradle.kts` following the Spine SDK - versioning policy. Use when starting a new branch, before opening a PR, or - when CI rejects a branch for a missing/insufficient version increment. Covers - locating the published version value, choosing the increment, committing the - bump, rebuilding reports, and resolving version conflicts. ---- - -# Bump the project version - -The authoritative policy is [Spine SDK Versioning][version-policy]. In this -skill's target repository, CI runs the `Version Guard` workflow, which invokes -`checkVersionIncrement` through `IncrementGuard`. The task fails if the current -project version already exists in the Maven repository. It does not compare git -branches or inspect commit subjects; the checks below are agent-side guardrails. - -## Commit authorization - -This skill is authorized to run `git commit` **exactly once** per invocation, -under these constraints: - -- Stage only `version.gradle.kts`. Any other modified files are out of scope - for this skill's commit and must remain unstaged. -- Use the exact subject `` Bump version -> `` `` (see step 4 of the - Checklist) with the actual new version value substituted. Keep the - backticks around the version literal (for example, ``... -> `2.0.0``` ) and - do not escape them as ``\````. -- No `git push`, `git tag`, `git rebase`, `git commit --amend`, or any other - history-writing operation. Those require a separate authorization - (`.agents/safety-rules.md` โ†’ *Commits and history-writing*). - -If the bump cannot be performed cleanly (no diff to commit, conflicting -staged files, build failures preceding the commit), report and stop โ€” do not -create the commit. - -## Checklist - -1. Work from the target repository root. - - Confirm `version.gradle.kts` exists before editing. If it is absent, stop and - report that this skill does not apply to the current checkout. - - Inspect `git status --short` before changing files. Preserve unrelated user - changes and stage only the version/report files this workflow owns. - -2. Locate `version.gradle.kts` and update the value that feeds - `versionToPublish`. - - The published version may be a literal: - - ```kotlin - val versionToPublish: String by extra("2.0.0-SNAPSHOT.182") - ``` - - Or it may come from another variable: - - ```kotlin - val compilerVersion: String by extra("2.0.0-SNAPSHOT.043") - val versionToPublish by extra(compilerVersion) - ``` - - In the second case, update the source value (`compilerVersion` here), not - only the `versionToPublish` alias. - -3. Choose the increment. - - For the normal snapshot-line PR, increment the trailing snapshot number by - one: `2.0.0-SNAPSHOT.182` -> `2.0.0-SNAPSHOT.183`. Preserve existing - zero-padding: `2.0.0-SNAPSHOT.009` -> `2.0.0-SNAPSHOT.010`. - - For a breaking snapshot-line PR, advance to the next multiple of 10 that is - strictly greater than the current value: `.187` -> `.190`, and `.180` -> - `.190`. - - For release-line work, follow the [policy][version-policy]: urgent fixes bump `PATCH`; - feature work or significant fixes bump `MINOR` and reset `PATCH` to `0`. - -4. Commit only the `version.gradle.kts` change with this subject: - - ```text - Bump version -> `2.0.0-SNAPSHOT.183` - ``` - - Shell-safe example (no escaped backticks in the commit subject): - - ```bash - git commit -m 'Bump version -> `2.0.0-SNAPSHOT.183`' -- version.gradle.kts - ``` - - Use the actual new version in the subject. Do not include unrelated files in - this commit. - -5. Run the build to verify the bump and regenerate reports: - - ```bash - ./gradlew clean build - ``` - - Repos using this config commonly finalize `generatePom` and - `mergeAllLicenseReports` after `build`, which updates - `docs/dependencies/pom.xml` and `docs/dependencies/dependencies.md` when - those reports are configured. - -6. If `docs/dependencies/pom.xml` or `docs/dependencies/dependencies.md` changed, - commit those generated files separately: - - ```text - Update dependency reports - ``` - - If the PR has the `License Reports` workflow, make sure the branch modifies - `docs/dependencies/pom.xml` and `docs/dependencies/dependencies.md`. - -7. Validate the branch state. - - ```bash - BASE=master - git fetch --quiet origin "$BASE" - RANGE="$(git merge-base HEAD origin/$BASE)..HEAD" - git log --format=%s "$RANGE" | grep '^Bump version ->' - git diff --name-only "$RANGE" -- version.gradle.kts | grep '^version.gradle.kts$' - ``` - - Use the actual merge target for `BASE` when it is not `master`. - Also confirm `git status --short` has no uncommitted changes created by the - version bump or report regeneration. - -## Conflict Rule - -When merging a base branch into a feature branch: - -- If the base branch version is lower, keep the feature branch version. -- If the base branch version is greater than or equal to the feature branch - version, set the feature branch version to `base + 1`, or apply the breaking - change rounding rule. - -Do not require a completely clean worktree if unrelated user changes are -present. Instead, make sure no uncommitted changes were created by the version -bump or report regeneration. - -[version-policy]: https://github.com/SpineEventEngine/documentation/wiki/Versioning diff --git a/.agents/skills/bump-version/agents/openai.yaml b/.agents/skills/bump-version/agents/openai.yaml deleted file mode 100644 index 12f6e4f9b8..0000000000 --- a/.agents/skills/bump-version/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Bump Version" - short_description: "Bump Spine project versions safely" - default_prompt: "Use $bump-version to bump the project version in version.gradle.kts, commit the version change, rebuild dependency reports, and verify the branch." diff --git a/.agents/skills/check-links/SKILL.md b/.agents/skills/check-links/SKILL.md deleted file mode 100644 index a4c61a0c5b..0000000000 --- a/.agents/skills/check-links/SKILL.md +++ /dev/null @@ -1,325 +0,0 @@ ---- -name: check-links -description: > - Validate the Hugo documentation site under `docs/` or `site/` for broken - links. Builds the site, starts the Hugo server locally, runs Lychee against - the rendered HTML using the repo's `lychee.toml`, and reports any broken URLs - grouped by source Markdown page. Use locally before pushing changes that - touch `docs/**` or `site/**`, when CI's `Check Links` job fails, or whenever - the user asks to "check doc links". Read-only with respect to the project - sources. Does **not** cover Javadoc/KDoc (out of scope for this skill). ---- - -# Check links in the Hugo docs (repo-specific) - -You are the documentation link checker for this Spine Event Engine project. -You build the site under `docs/` or `site/` (auto-detected; see step 0), serve -it locally on port `1414`, run Lychee against the rendered HTML, and report -broken URLs. You mirror what the `.github/workflows/check-links.yml` workflow -does in CI: same Hugo version, same Lychee version, same Hugo environment -(`development`), and the same `lychee.toml`. Two deliberate differences remain: -the skill serves on port `1414` (CI uses `1313`) to avoid clashing with a -developer's local `hugo server`, and the skill writes a local sentinel that CI -does not. Both differences are harmless because `--base-url` is rewritten to -match the local port and the sentinel is consumed only by the local `pre-pr` -skill. - -### Pinned versions - -`.github/workflows/check-links.yml` is the **single source of truth** for the -Hugo and Lychee pins. This file does not duplicate the current values -because duplicates inevitably drift; see the workflow's `env:` block for -the canonical `HUGO_VERSION` and `LYCHEE_VERSION_TAG`. The auto-download -step (ยง2) reads `LYCHEE_VERSION_TAG` out of the workflow at runtime, so a -workflow bump propagates automatically. Hugo is not auto-installed; the -skill uses whichever `hugo` is on `$PATH` and only warns (does not block) -if the installed version is older than the workflow's `HUGO_VERSION` โ€” -Hugo's HTML output is stable enough across minor versions that a small -skew does not invalidate link-check results. - -The authoritative shared config is `lychee.toml` at the repo root. Do not -fork its exclude list โ€” fix the source link or, if the failing URL is a known -flaky external endpoint, add it to `lychee.toml` once (the change applies to -both the skill and CI). - -## When to run - -- Any change touches `docs/**` or `site/**` (including reference links, - `embed-code` blocks, sidenav YAML files, content under `/content/`). -- A change touches `lychee.toml` itself. -- CI reported broken links and you want a fast local repro. -- The user asks to "check the doc links" or invokes `/check-links`. - -If none of the above is true, decline with a one-line note rather than -running the (~30 s) build+check. - -## Tooling - -The skill needs four binaries: - -| Tool | Purpose | Install hint | -|--------|------------------------------------------|-------------------------------| -| Hugo | Build and serve the site | `brew install hugo` (extended)| -| Node | Hugo theme dependencies (`npm ci`) | `brew install node` | -| npm | Same | bundled with Node | -| Lychee | Link checker | `brew install lychee` | - -For **Lychee**, prefer a pre-installed binary on `$PATH`. If none is found, -download the pinned release (see `LYCHEE_VERSION_TAG` in -`.github/workflows/check-links.yml` โ€” the dynamic-read pattern in step 2 below -keeps this version in lock-step with CI) into -`.agents/skills/check-links/.cache/lychee/` and use that path. The pinned -version matches what the CI workflow uses, so behavior is identical. - -`.agents/skills/check-links/.cache/` is git-ignored (see `.gitignore`). - -## Procedure - -Execute the steps in order. On the first failure, stop, write a `FAIL` -sentinel (step 8), and report the failure with the next action. - -### 0. Detect site root and work directory - -Before any other step, determine `SITE_DIR` (the Hugo site root) and `WORK_DIR` -(the directory where `npm ci` / `hugo` commands run โ€” mirrors `.github/workflows/check-links.yml`): - -```bash -SITE_DIR="" -for dir in docs site; do - for cfg in hugo.toml hugo.yaml \ - config/hugo.toml config/hugo.yaml \ - config/_default/hugo.toml config/_default/hugo.yaml; do - if [ -f "$dir/$cfg" ]; then - SITE_DIR="$dir" - break 2 - fi - done -done -if [ -z "$SITE_DIR" ]; then - echo "ERROR: No Hugo config found under docs/ or site/." >&2 - exit 1 -fi - -if [ -f "${SITE_DIR}/_preview/package-lock.json" ]; then - WORK_DIR="${SITE_DIR}/_preview" -elif [ -f "${SITE_DIR}/package-lock.json" ]; then - WORK_DIR="${SITE_DIR}" -else - echo "ERROR: No package-lock.json found under ${SITE_DIR}/_preview/ or ${SITE_DIR}/." >&2 - exit 1 -fi -``` - -Use `$SITE_DIR` for content paths and `$WORK_DIR` for build/serve operations in the steps below. - -### 1. Scope check - -Run `git diff ...HEAD --name-only` (default `` = `master` unless -the user provides another). If the change set has **no** files under -`$SITE_DIR/**` and no changes to `lychee.toml`, and the user did not -explicitly ask, decline and exit cleanly. - -### 2. Preflight binaries - -- `hugo version` โ†’ must succeed; capture the version. If missing, stop with - Must-fix: "Install Hugo extended (`brew install hugo`)." If installed but - older than the workflow's `HUGO_VERSION` (parse with - `grep -E '^[[:space:]]+HUGO_VERSION:' .github/workflows/check-links.yml | sed -E 's/.*: *"?([^"]+)"?$/\1/'`), warn but - continue. -- `node -v` and `npm -v` โ†’ must succeed. If missing, stop with Must-fix: - "Install Node (`brew install node`) at the major version pinned by - `node-version:` in `.github/workflows/check-links.yml`." -- `lychee --version` โ†’ if it succeeds, record the path and version. -- If `lychee` is missing: - 1. Read the canonical pin from the workflow file so the skill cannot drift - from CI: - ```bash - LYCHEE_VERSION_TAG=$( - grep -E '^[[:space:]]+LYCHEE_VERSION_TAG:' .github/workflows/check-links.yml \ - | sed -E 's/.*: *"?([^"]+)"?$/\1/' - ) - ``` - Expected shape: `lychee-vX.Y.Z` (the leading `lychee-` is part of the - upstream release tag, not a typo). - 2. Determine platform via `uname -s` / `uname -m`. Map to the matching - Lychee asset (recent releases โ€” `v0.24.2` and later โ€” drop the - version from the asset filename): - - `Darwin` + `arm64` โ†’ `lychee-aarch64-apple-darwin.tar.gz` - - `Darwin` + `x86_64` โ†’ `lychee-x86_64-apple-darwin.tar.gz` - - `Linux` + `x86_64` โ†’ `lychee-x86_64-unknown-linux-gnu.tar.gz` - - `Linux` + `aarch64` โ†’ `lychee-aarch64-unknown-linux-gnu.tar.gz` - - any other combination (e.g. Windows, FreeBSD, 32-bit) โ†’ stop with - Must-fix: "Unsupported platform for Lychee auto-download โ€” install - Lychee manually (`brew install lychee` / `cargo install lychee`) - and rerun." - 3. Ensure the cache directory exists *before* the download โ€” - `mkdir -p .agents/skills/check-links/.cache/lychee/` โ€” - because the path is git-ignored and absent on a fresh clone, - and `tar -xzf โ€ฆ -C

` will fail with "no such file or - directory" if the target does not exist yet. This mirrors the - `mkdir -p lychee` that `check-links.yml` does before its own - extract step. - 4. Download from - `https://github.com/lycheeverse/lychee/releases/download/${LYCHEE_VERSION_TAG}/` - into `.agents/skills/check-links/.cache/lychee/` and extract - with `tar -xzf --strip-components=1 -C .agents/skills/check-links/.cache/lychee/` - so the binary lands at - `.agents/skills/check-links/.cache/lychee/lychee`. - 5. Use `.agents/skills/check-links/.cache/lychee/lychee` for the rest of this run. - 6. Print a one-line note: "Using auto-downloaded Lychee. For faster runs, - install with `brew install lychee`." - -### 3. Install Hugo deps - -Run `( cd ${WORK_DIR} && npm ci )`. We deliberately use `npm ci` -(matching the CI workflow's `Install Dependencies` step in `check-links.yml`) -rather than `npm install`: - -- `npm ci` installs exactly the versions pinned by `package-lock.json`; - `npm install` is allowed to update the lockfile and may resolve to - different transitive versions than CI, which defeats the "render - identical HTML to CI" goal. -- If `package.json` and `package-lock.json` drift out of sync, `npm ci` - fails fast with a clear error rather than silently healing the - lockfile โ€” a divergence we want to surface, not paper over. - -### 4. Build the site - -Run `( cd ${WORK_DIR} && hugo -e development )`. -This emits `${WORK_DIR}/public/**/*.html`. The `-e development` flag matches -what CI uses in `check-links.yml` so the two builds render identical HTML. -(The helper `${SITE_DIR}/_script/hugo-build` exists for interactive use but -defaults to `production`; we invoke `hugo` directly to keep the env in -lock-step with CI.) - -### 5. Start the Hugo server in the background - -The server must survive across multiple `Bash` tool calls (steps 5 โ†’ 6 โ†’ 8 -typically run in separate shells), so we rely on `nohup` alone โ€” a `trap โ€ฆ -EXIT` would fire when *this* shell exits and kill the server before Lychee -can query it. Teardown happens explicitly in step 8. - -Before launching, kill any leftover server from a previous crashed run so a -stale process does not hold port `1414`: - -```bash -pkill -F /tmp/check-links.hugo.pid 2>/dev/null || true -rm -f /tmp/check-links.hugo.pid - -( cd ${WORK_DIR} && nohup hugo server --environment development --port 1414 \ - > /tmp/check-links.hugo.out 2>&1 & echo $! > /tmp/check-links.hugo.pid ) -sleep 5 - -# Verify the captured PID is alive before relying on it. `$!` for -# `nohup foo &` is reliable on bash but not portable across shells; the -# pgrep check turns a silent "Lychee fetches an empty port" failure into -# a clear error. -if ! pgrep -F /tmp/check-links.hugo.pid > /dev/null 2>&1; then - echo "ERROR: Hugo server failed to start. Tail of log:" >&2 - tail -20 /tmp/check-links.hugo.out >&2 || true - exit 1 -fi -``` - -Port `1414` is chosen to avoid clashing with a developer's local `hugo server` -(default `1313`). The `--environment development` flag matches CI's build env. - -### 6. Run Lychee - -```bash - --config lychee.toml --timeout 60 \ - --base-url http://localhost:1414/ \ - "${WORK_DIR}/public/**/*.html" -``` - -Capture exit code. Any non-zero exit means at least one broken link. - -### 7. Report - -Group the broken URLs from Lychee's output by source page. To reverse-map -an HTML path to its Markdown source: - -`${WORK_DIR}/public/docs/
//index.html` -โ†” `${SITE_DIR}/content/docs/
/.md` (or `/_index.md`). - -Report in this shape: - -``` -## Doc link check ( vs ) - -Hugo: -Lychee: () -Pages scanned: -Broken URLs: - -### /content/docs/<...>/.md -- โ€” -- โ€” ... - -### /content/docs/<...>/.md -- ... -``` - -If `K == 0`, report a single line: "All links OK." - -### 8. Tear down and sentinel - -- Kill the Hugo server (and clean up its pid file): - - ```bash - pkill -F /tmp/check-links.hugo.pid 2>/dev/null || true - rm -f /tmp/check-links.hugo.pid /tmp/check-links.hugo.out - ``` - - Run this even if Lychee failed โ€” leaving a server on port `1414` would - poison the next invocation. -- Write `.git/check-links.ok` at the repo root: - - ``` - head= - branch= - status=PASS|FAIL - timestamp= - hugo= - lychee= - pages= - broken= - ``` - -The sentinel is consumed by the `pre-pr` skill's reviewer step: when it -sees a sentinel whose `head=` matches the current HEAD SHA and -`status=PASS`, it skips re-dispatching `check-links` and records it -as APPROVE with the note "cached from `.git/check-links.ok`". Any -HEAD advance (commit, amend, rebase) invalidates the cache automatically. - -## Notes - -- This skill does **not** modify tracked sources. It does, however, write - several git-ignored build artifacts during a run โ€” listed here so a future - reader does not mistake them for unrelated side-effects: - - `.agents/skills/check-links/.cache/lychee/` โ€” auto-downloaded - Lychee binary, when the system Lychee was unavailable. - - `${WORK_DIR}/node_modules/` โ€” installed by `npm ci` in step 3. - - `${WORK_DIR}/public/` โ€” Hugo's rendered HTML (the corpus Lychee scans). - - `${WORK_DIR}/resources/` โ€” Hugo's asset-pipeline cache. - - `.lycheecache` at the repo root โ€” Lychee's per-URL result cache - (honoured for `max_cache_age = "3d"` per `lychee.toml`). - - `/tmp/check-links.hugo.{pid,out}` โ€” server PID file and log, both - removed in step 8's teardown. - - Every path above is matched by an existing `.gitignore` entry; none is - committed. -- The `lychee.toml` exclude list is the single source of truth for flaky - external endpoints. If a real link must be excluded, add it there and - explain why in a comment so CI and local runs stay in sync. -- The skill assumes the docs build succeeds. A Hugo build error is treated - the same as a link failure โ€” surface it and stop. -- The `include_verbatim = false` setting in `lychee.toml` skips links inside - code blocks. That is intentional today; flip it on if you specifically need - to validate examples. - -## Related skills - -- `review-docs` โ€” prose, KDoc/Javadoc, and Markdown style review. Runs in - parallel with `check-links` when invoked by `pre-pr`. -- `pre-pr` โ€” composes the above and gates `gh pr create`. diff --git a/.agents/skills/dependency-audit/SKILL.md b/.agents/skills/dependency-audit/SKILL.md deleted file mode 100644 index 010c16bced..0000000000 --- a/.agents/skills/dependency-audit/SKILL.md +++ /dev/null @@ -1,146 +0,0 @@ ---- -name: dependency-audit -description: > - Audit changes to dependency declarations under - `buildSrc/src/main/kotlin/io/spine/dependency/` โ€” catches accidental - version downgrades, BOM mismatches, missing deprecation markers when - artifacts are renamed or removed, copyright drift, and convention drift. - Use whenever a diff touches that directory, or when asked to "audit - this dependency bump". Read-only; does not run builds. ---- - -# Dependency audit (repo-specific) - -You are the dependency auditor for a Spine Event Engine repo. All managed -dependencies live under: - - buildSrc/src/main/kotlin/io/spine/dependency/ - -organized by sub-package: - -- `lib/` โ€” third-party runtime libraries (Kotlin, Guava, Protobuf, gRPC, โ€ฆ). -- `local/` โ€” Spine SDK artifacts (Base, CoreJvm, ModelCompiler, โ€ฆ). -- `test/` โ€” testing libraries (JUnit, Kotest, AssertK, Truth, Jacoco, Kover). -- `build/` โ€” static-analysis and build-time tools (Dokka, ErrorProne, Pmd, - CheckStyle, KSP, โ€ฆ). -- `kotlinx/` โ€” Kotlin-ecosystem libraries (Coroutines, Serialization, - DateTime, AtomicFu). -- `boms/` โ€” BOM declarations. - -Each file declares a Kotlin `object` extending `Dependency` or `DependencyWithBom` -(see `dependency/Dependency.kt`). The shape is: - - object Kotest { - const val version = "6.1.11" - const val group = "io.kotest" - const val assertions = "$group:kotest-assertions-core:$version" - // โ€ฆ - } - -## How to run an audit - -1. **Fetch the full diff once.** Default base is `origin/master`: - `git diff origin/master...HEAD -- 'buildSrc/src/main/kotlin/io/spine/dependency/**'` - (use `--staged` if the user is mid-commit, or a different base only if - the user names one). The unified diff already contains the old and new - lines you need for version-sanity and BOM checks โ€” do not call `--stat` - first and then re-read each file. If the diff is empty, ask the user - which files to audit. - -2. **Lean on the diff; `Read` on demand.** Version, BOM, copyright, and - deprecation deltas are all visible in the unified diff. Only `Read` a - file when (a) it is newly added, or (b) a hunk references a - `version`/`group` constant defined outside the hunk and you need - surrounding context. **Budget:** if more than 5 files changed, do not - `Read` individual files โ€” work from the diff and use targeted `Grep` - for cross-cutting questions. - -3. **Batch independent work into one turn.** Issue the version-sanity (A), - convention-drift (D), and cross-cutting (E) tool calls *in parallel* - within a single response. Collect every finding and emit the report - once โ€” **do not stop at the first failure**. - -4. **Batch greps.** For deprecation/caller checks (C) and snapshot-pin - checks (A), build one ripgrep over the union of symbols instead of one - command per symbol. Examples: - - `rg -n '\b(name1|name2|name3)\b' --type kt` to find callers of any - removed `const val`. - - `rg -L 'Copyright \(c\) 2026' ` to flag every stale - header in one call. - - `rg -L '@Suppress\("unused", "ConstPropertyName"\)' ` - to flag missing object-level suppression in one call. - - `rg -n '(lib1:oldv1|lib2:oldv2)' --type kt --type gradle` โ€” one - alternation across libraries, not one command per library. - -5. **Fast path for pure version bumps.** If every hunk only modifies an - existing `version` (or `bom`) string literal โ€” no added/removed - `const val`, no new files, no renames โ€” run only Checks A and D. - Skip B, C, and E entirely. This is the dominant `/dependency-update` - shape; do not waste tool calls re-validating naming or deprecation - discipline when nothing structural changed. - -## Checks - -### A. Version sanity -- **No silent downgrade.** Compare the old and new `version` value as semver. - A decrease (`2.0.0 -> 1.9.0`) or a snapshot regression (`-SNAPSHOT.183` -> - `.182`) is a Must-fix unless the commit message explicitly justifies it. -- **Snapshot vs. release consistency.** If `version` switches from a release - (`2.0.0`) to a snapshot (`2.0.1-SNAPSHOT.001`), confirm the consuming code - isn't pinned to the release elsewhere. Use the batched ripgrep recipe - in step 4 โ€” one alternation across all switched libraries, not one - command per library. -- **BOM โ†” component agreement.** For objects extending `DependencyWithBom`, - check that `bom` references the same version as `version` (e.g. Kotlin's - `kotlin-bom:$runtimeVersion`). - -### B. Naming and structure -- **Object name matches the upstream library name** (PascalCase). New files - must follow the convention of neighbors (e.g. `lib/Foo.kt` declares - `object Foo`). -- **No type names in property names** (`fooList`, `barObject`) โ€” this is in - `.agents/coding-guidelines.md`. -- **Module constants use `"$group::$version"`**, not hardcoded - Maven coordinates. Catch copy-paste like `"io.kotest:kotest-assertions-core:6.1.11"`. - -### C. Deprecation discipline -When an artifact is **renamed or removed**: -- The old `const val` must stay with `@Deprecated("โ€ฆ", ReplaceWith("โ€ฆ"))` - or `@Deprecated("โ€ฆ")` (see `Kotest.frameworkApi` and `Kotest.datatest` for - the established style). -- If the diff deletes one or more `const val`s outright, confirm no caller - is left behind. Use the batched ripgrep recipe in step 4 โ€” one - alternation over all removed symbol names, not one `git grep` per - name. If any caller survives, this is a Must-fix. - -### D. Convention drift -- **Copyright header year.** Every changed file should have a current-year - copyright line. If a file was edited but its copyright says `2024`, flag it - (the user can run `/update-copyright` to fix). -- **GitHub URL comment.** New `lib/` and `kotlinx/` files conventionally - start with `// https://github.com//` above the object. - Recommend it if missing. -- **`@Suppress("unused", "ConstPropertyName")` on the object.** This is the - established style for constant-heavy declarations. - -### E. Cross-cutting checks -- **`local/` deps don't leak.** Spine SDK artifacts in `local/` should not be - declared in `lib/` or `test/` (and vice versa). -- **No mixing Groovy and Kotlin DSL.** All Gradle code in `buildSrc/` must be - `.kt` or `.gradle.kts`. Catch any `.gradle` file slipping in. - -## Output format - -Three sections, in this order: - -- **Must fix** โ€” version downgrades, missing deprecation markers on removed - symbols, broken callers, BOM/version mismatches. -- **Should fix** โ€” convention drift, missing deprecation `ReplaceWith`, - missing copyright update, missing URL comment, naming oddities. -- **Nits** โ€” formatting, ordering, doc-comment polish. - -For each finding, cite the file and line, quote the offending lines, and -show the recommended fix. - -End with a one-line verdict: `APPROVE`, `APPROVE WITH CHANGES`, or -`REQUEST CHANGES`. diff --git a/.agents/skills/dependency-update/SKILL.md b/.agents/skills/dependency-update/SKILL.md deleted file mode 100644 index 7e70bc126c..0000000000 --- a/.agents/skills/dependency-update/SKILL.md +++ /dev/null @@ -1,283 +0,0 @@ ---- -name: dependency-update -description: > - Walk every dependency declaration under - `buildSrc/src/main/kotlin/io/spine/dependency/`, discover the latest accepted - version of each artifact from the URL hinted in its file (or from Maven - metadata if no URL is present), and update the `version` constant in place. - External dependency scopes accept only released versions; the `local` scope - also accepts snapshots and pre-releases published from sibling Spine repos. - Use when asked to refresh dependency versions, bump libraries, run a - dependency audit, or "see what's stale". ---- - -# Update dependencies - -## Goal - -Bring every dependency object under -`buildSrc/src/main/kotlin/io/spine/dependency/` to its latest accepted version. -For every scope except `local/`, that means the latest **released** version: -snapshots, release candidates, milestones, alpha/beta, EAP, and `-dev` builds -are **excluded**. - -`local/` is the deliberate exception. It holds Spine SDK dependencies published -from sibling Spine repositories, and it may move to newer snapshots or -pre-releases such as `2.0.0-SNAPSHOT.388` or `2.1.0-RC1`. - -The authoritative version source for each artifact is the web page already -referenced in its file. When the file has no URL, use the Maven metadata -fallback described below. For non-`local/` artifacts, a discovered Maven -Central URL is **added back to the file** as a line comment so the next run has -a hint. - -## Inputs - -- No arguments โ†’ scan all of `buildSrc/src/main/kotlin/io/spine/dependency/`. -- One or more paths or sub-package names (`lib`, `local`, `test`, `build`, - `kotlinx`, `boms`) โ†’ restrict the scan to those. -- `--dry-run` โ†’ discover and report, but do not edit. - -## Pre-flight - -1. Run `git status --short`. If the worktree is dirty in files this skill will - touch, stop and ask the user. Otherwise preserve unrelated changes. -2. Confirm `buildSrc/src/main/kotlin/io/spine/dependency/` exists. -3. Note the current branch โ€” every change this skill makes is a candidate for - a single `chore(deps): refresh external versions` commit at the end; the - skill itself does NOT commit. The user decides. - -## Per-file workflow - -For each `*.kt` file in scope: - -### 1. Parse the file - -A dependency file declares one or more Kotlin `object`s, typically extending -`Dependency` or `DependencyWithBom`. The shape is: - - object Kotest { - const val version = "6.1.11" - const val group = "io.kotest" - const val assertions = "$group:kotest-assertions-core:$version" - // โ€ฆ - } - -Extract: - -- `objectName` โ€” the outer `object` identifier. -- `version` โ€” the literal version string. Some files have **multiple** version - constants (`runtimeVersion`, `embeddedVersion`, `annotationsVersion`); treat - each separately. The one driving the artifact is typically `override val - version = โ€ฆ` or the `const val version = โ€ฆ` declared at the top. -- `group` โ€” the Maven group. -- `module` artifact names โ€” each `const val foo = "$group:foo:$version"` line - contributes one artifact name. Use the first one to query Maven Central if - needed for non-`local/` artifacts, or Spine SDK Maven repositories for - `local/` artifacts. -- `versionUrl` โ€” a URL hint. Look in this order: - 1. Line comments above the object: `^//\s*(https?://\S+)`. - 2. KDoc `@see โ€ฆ` inside the object's KDoc. - 3. Plain `@see https?://โ€ฆ` inside the KDoc. - 4. If none: leave `versionUrl` empty and use the Maven metadata fallback - below. - -Skip files that contain only abstract base classes or helpers (`Dependency.kt`, -`DependencyWithBom.kt`, `BomsPlugin.kt`, anything without a concrete artifact -declaration). - -### 2. Find the latest accepted version - -The discovery rule depends on the URL shape. For files under -`dependency/local/`, check the Spine SDK Maven metadata before GitHub, even -when the file has a GitHub URL; snapshots are usually visible in Maven -metadata, not in GitHub's latest-release redirect. - -**A. GitHub repository URL** (`https://github.com//`): - -- Outside `local/`, resolve - `https://github.com///releases/latest`. GitHub redirects to the - latest non-prerelease tag. Read the redirected location or the rendered HTML - to extract the tag. -- In `local/`, do **not** rely on `/releases/latest`, because it hides - pre-releases. Use GitHub releases and tags only after checking Spine SDK - Maven metadata. When you do use GitHub, include pre-release entries and keep - version-like tags that match the artifact. -- Tags often have a `v` prefix. Strip it. -- If the repo publishes per-component tags (e.g. - `kotlinx-coroutines-1.10.2`), prefer the tag whose name matches the - artifact's module identifier. Otherwise take the topmost release. - -**B. Maven Central artifact URL** -(`https://search.maven.org/artifact//` or -`https://repo1.maven.org/maven2///`): - -- Hit Maven Central's REST API: - `https://search.maven.org/solrsearch/select?q=g:+AND+a:&rows=20&core=gav` -- Outside `local/`, filter the `response.docs[].v` values by the pre-release - rule (below). -- In `local/`, keep snapshots and pre-releases in the candidate list. -- Take the highest by semver comparison. - -**C. Spine SDK Maven repositories for `local/` artifacts**: - -- For files under `dependency/local/`, query Maven metadata in the current - Spine SDK Artifact Registry repositories before falling back elsewhere: - - `https://europe-maven.pkg.dev/spine-event-engine/releases` - - `https://europe-maven.pkg.dev/spine-event-engine/snapshots` -- Build the metadata URL as - `///maven-metadata.xml`, where `groupPath` is the - Maven group after first resolving symbolic aliases used in dependency files - (for example, `Spine.group` -> `io.spine` and `Spine.toolsGroup` -> - `io.spine.tools`) and then replacing dots with slashes. -- Read `...` entries. For `local/`, do not - reject `SNAPSHOT`, RC, milestone, alpha, beta, EAP, pre, or dev versions. -- If both release and snapshot repositories have candidates, compare all of - them together and take the highest version. - -**D. Project homepage** (e.g. `https://kotest.io/`, `https://junit.org/`, -`https://www.detekt.dev/`): - -- Try to find a "latest release" or "download" link on the page. If the page - is a thin landing page with no usable version data, fall through to E. - -**E. No URL or unusable URL โ€” Maven metadata fallback**: - -- Outside `local/`, query Maven Central as in B using the file's `group` and - the first module artifact name (the part after `$group:`). -- In `local/`, query the Spine SDK Maven metadata first. Use Maven Central only - if the artifact is absent from those repositories. -- If a non-`local/` Maven Central fallback query returns results, **also insert - a line comment** - `// https://search.maven.org/artifact//` above the object - declaration (after any existing copyright header). This back-fills the URL - hint for next time. Match the existing comment style (one line, no trailing - punctuation). -- If all fallback queries have no result, leave the file untouched and add it - to the `Manual review` section of the final report. - -### 3. Filter pre-releases outside `local/` - -Apply this filter only to files outside `dependency/local/`. - -For `local/` files, snapshots and pre-releases are accepted candidates. Do not -put them in `Filtered pre-releases`; put them in the `local/` confirmation -section of the final report instead. - -Reject any version string matching, case-insensitively: - - -SNAPSHOT$ - -RC[\d\-.]*$ e.g. -RC1, -RC.2 - -M\d+$ e.g. -M3 - -alpha[\d\-.]*$ - -beta[\d\-.]*$ - -EAP[\d\-.]*$ - -pre[\d\-.]*$ - -dev[\d\-.]*$ - \.Beta\d*$ Spring-style trailing tokens - \.Alpha\d*$ - \.RC\d*$ - \.M\d+$ - -Apply the regex to the **suffix after the numeric version**. The version -`2.0.0-SNAPSHOT.182` is a snapshot and must be rejected as a target outside -`local/`, but it is valid for `local/` dependency objects. This skill only -edits dependency files, never `version.gradle.kts` (that belongs to the -`bump-version` skill). - -### 4. Compare versions - -Use semver comparison: - -- Split on `.` and `-`. -- Numeric segments compare numerically; non-numeric segments compare - lexicographically. -- A version without any pre-release suffix is greater than one with the same - numeric prefix but a pre-release suffix. - -Only update when `latest > current`. Equal or lower โ†’ no change. - -### 5. Apply the edit - -- Replace the `version` literal with the new value. Use a precise replacement - anchored on the full line (`const val version = ""` โ†’ - `const val version = ""`). Do not blindly replace the version string, - because the same string can appear in module URLs constructed via - interpolation (`"$group:โ€ฆ:$version"`) โ€” those will pick up the new value - automatically. -- If the file uses a renamed version constant (`runtimeVersion`, - `compilerVersion`, etc.) that feeds `override val version = compilerVersion`, - update the **source** constant, not the alias. -- For `DependencyWithBom` objects, verify the `bom` line still resolves - correctly. The conventional shape is - `override val bom = "$group:-bom:$version"`, in which case no - separate edit is needed. If the BOM version is hard-coded, update it too. -- Preserve indentation, comment style, and surrounding blank lines exactly. - -### 6. Watch for `local/` artifacts - -`local/` holds Spine SDK dependencies (Base, CoreJvm, ModelCompiler, โ€ฆ) that -are published from sibling Spine repos. This scope accepts snapshots and -pre-releases because these artifacts often advance through internal snapshot -builds before a stable SDK release. - -Still **flag every `local/` update in the report**, and note whether the target -is a release, snapshot, or pre-release. The user can then decide whether to -bump the SDK in lockstep with the rest of the project. Spine SDK artifacts -often need to move together; one-off bumps can cause runtime ABI mismatches. - -## Report - -When the run completes, emit a Markdown report with these sections: - -- **Updated** โ€” table of `file | objectName | old โ†’ new | source URL`. -- **Already current** โ€” file/object pairs whose version was already the - newest accepted version. -- **Skipped (no URL, metadata empty)** โ€” manual review needed. -- **Filtered pre-releases** โ€” newer versions found but rejected because they - were RC/SNAPSHOT/alpha/etc. Applies only outside `local/`. -- **`local/` bumps to confirm** โ€” every `local/` change called out separately, - including snapshot and pre-release targets. - -End with the suggested next steps: - -1. Review the diff (`git diff buildSrc/src/main/kotlin/io/spine/dependency/`). -2. Invoke `/version-bumped`. Every feature branch must advance - `version.gradle.kts` strictly above the base before any - `./gradlew build` (which may transitively `publishToMavenLocal`). The - skill is a no-op when a bump already happened earlier on the branch - and otherwise calls `/bump-version` to perform the increment. -3. Run `./gradlew build` (or `./gradlew clean build` if `.proto` files - participate). -4. Commit. Match the shape of the actual change: - - Single `local/` bump (most common): `` Bump Spine Base -> `2.0.0-SNAPSHOT.190` `` - - Coordinated external set: `Bump Protobuf and gRPC` (one commit; - mention both). - - Bulk external refresh (rare): `Refresh external dependencies`. - -## Safety - -- Do not commit. Do not push. Editing files is the limit of this skill's - authority. -- Never edit `version.gradle.kts` โ€” that's the `bump-version` skill's - responsibility. -- Never auto-resolve a Maven Central query that returns multiple matching - artifacts with different groups (e.g. a library that exists under both - `io.netty` and `io.netty.incubator`). Ask the user. -- If a discovered "latest" version is more than one **major** ahead of the - current value (e.g. `1.x` โ†’ `3.x`), flag it as a major bump in the report - and apply the edit only if the user confirms, or only when running - non-interactively with `--include-majors`. Major bumps frequently break - ABI. - -## Failure modes to expect - -- **GitHub rate limit** on the unauthenticated REST API. The `/releases/latest` - HTML page does not require auth and is the preferred fallback. -- **Per-component tags** in a monorepo. Match by artifact name, don't take the - topmost tag blindly. -- **Repositories that publish to JCenter only** โ€” JCenter is sunset; if Maven - Central is empty, the dependency may need migration. Flag it. -- **Vendor-specific version schemes** (e.g. dates: `2025.10.01`) โ€” the - semver comparator above will still order these correctly; just don't - mis-classify them as pre-releases. diff --git a/.agents/skills/dependency-update/agents/openai.yaml b/.agents/skills/dependency-update/agents/openai.yaml deleted file mode 100644 index a61198d327..0000000000 --- a/.agents/skills/dependency-update/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Dependency Update" - short_description: "Refresh dependency versions, allowing snapshots only for local Spine SDK artifacts" - default_prompt: "Use $dependency-update to walk every dependency object under buildSrc/src/main/kotlin/io/spine/dependency/, find the latest accepted version for each, and update the version constants in place. External scopes use released non-snapshot versions only; dependency/local/ may use snapshots and pre-releases from sibling Spine repos. Use the URL referenced in each file as the source of truth; fall back to Maven metadata and back-fill missing hints when useful." diff --git a/.agents/skills/java-to-kotlin/SKILL.md b/.agents/skills/java-to-kotlin/SKILL.md deleted file mode 100644 index b9835f8f7a..0000000000 --- a/.agents/skills/java-to-kotlin/SKILL.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -name: java-to-kotlin -description: > - Convert Java code to Kotlin, including Java API comments from Javadoc to KDoc. - Use when asked to migrate Java files, classes, methods, nullability semantics, - or common Java patterns into idiomatic Kotlin while preserving behavior. ---- - -# ๐Ÿช„ Converting Java code to Kotlin - -* Java code API comments are Javadoc format. -* Kotlin code API comments are in KDoc format. - -## Javadoc to KDoc conversion - -* The wording of original Javadoc comments must be preserved. - -## Treating nullability - -* Use nullable Kotlin type only if the type in Java is annotated as `@Nullable`. - -## Efficient Conversion Workflow - -* First, analyze the entire Java file structure before beginning conversion to understand dependencies and class relationships. -* Convert Java code to Kotlin systematically: imports first, followed by class definitions, methods, and finally expressions. -* Preserve all existing functionality and behavior during conversion. -* Maintain original code structure and organization to ensure readability. - -## Common Java to Kotlin Patterns - -* Convert Java getters/setters to Kotlin properties with appropriate visibility modifiers. -* Transform Java static methods to companion object functions or top-level functions as appropriate. -* Replace Java anonymous classes with Kotlin lambda expressions when possible. -* Convert Java interfaces with default methods to Kotlin interfaces with implementations. -* Transform Java builders to Kotlin DSL patterns when appropriate. - -## Error Prevention - -* Pay special attention to Java's checked exceptions versus Kotlin's unchecked exceptions. -* Be cautious with Java wildcards (`? extends`, `? super`) conversion to Kotlin's `out` and `in` type parameters. -* Ensure proper handling of Java static initialization blocks in Kotlin companion objects. -* Verify that Java overloaded methods convert correctly with appropriate default parameter values in Kotlin. -* Remember that Kotlin has smart casts which can eliminate explicit type casting needed in Java. - -## Documentation Conversion - -* Convert `@param` to `@param` with the same description. -* Convert `@return` to `@return` with the same description. -* Convert `@throws` to `@throws` with the same description. -* Convert `{@link}` to `[name][fully.qualified.Name]` format. -* Convert `{@code}` to inline code with backticks (`). - -## Final step: ensure the version is bumped - -After the conversion is verified, invoke `/version-bumped` so the branch -carries a strictly greater `version.gradle.kts` than the base ref before -any `./gradlew build` (which may transitively `publishToMavenLocal` and -overwrite the previously published snapshot consumer repos depend on). -The skill is a no-op when a bump already happened earlier on the branch. diff --git a/.agents/skills/java-to-kotlin/agents/openai.yaml b/.agents/skills/java-to-kotlin/agents/openai.yaml deleted file mode 100644 index 252920fedc..0000000000 --- a/.agents/skills/java-to-kotlin/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Java to Kotlin" - short_description: "Convert Java code to idiomatic Kotlin" - default_prompt: "Use $java-to-kotlin to convert Java code to Kotlin while preserving behavior, nullability, and API documentation wording." diff --git a/.agents/skills/kotlin-review/SKILL.md b/.agents/skills/kotlin-review/SKILL.md deleted file mode 100644 index 6cbca1afe8..0000000000 --- a/.agents/skills/kotlin-review/SKILL.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -name: kotlin-review -description: > - Review Kotlin (and Java) changes in this repo against the Spine coding - guidelines, safety rules, and testing policy. Use after any non-trivial - code edit, before opening a PR, or when asked for a code review. - Read-only; does not run builds. ---- - -# Kotlin code review (repo-specific) - -You are the Kotlin reviewer for this repository. The authoritative standards -live in `.agents/`: - -- `.agents/coding-guidelines.md` โ€” Kotlin idioms, formatting, what to prefer/avoid. -- `.agents/safety-rules.md` and `.agents/advanced-safety-rules.md` โ€” hard constraints - (no reflection without approval, no analytics/telemetry, no blocking calls in - coroutines, no auto-updating external dependencies). -- `.agents/testing.md` โ€” Kotest assertions preferred, stubs not mocks. -- `.agents/project-structure-expectations.md` โ€” module/source-set layout. -- `.agents/version-policy.md` โ€” version bumps are required only when the - repository has a root `version.gradle.kts`. - -## Review procedure - -1. Read the diff. Use `git diff --staged` or `git diff ...HEAD` depending on - what the user describes. Do NOT review the full repo โ€” only what changed. - Filter out config-distributed files (see `AGENTS.md ยง Code review` for the - exact list) before proceeding. If nothing remains after filtering, return - `APPROVE โ€” all changes are config-distributed files.` and stop. -2. Read each affected file fully, not just the diff hunks. Smart casts, - nullability, and idiomatic refactors require surrounding context. -3. Check against `.agents/coding-guidelines.md`: - - Kotlin idioms (extension functions, `when`, smart casts, data/sealed classes). - - Kotlin Protobuf DSL (`message { ... }`) preferred over Java builders (`newBuilder()`, `toBuilder()`) in Kotlin. - - Immutability by default. - - No `!!` without justification. - - No type names in variable names. - - No string duplication โ€” use companion-object constants. - - No mixing Groovy/Kotlin DSL in build logic. - - No double empty lines (collapse to a single empty line); no trailing whitespace. -4. Check safety rules: reflection, telemetry, blocking-in-coroutines, dependency - bumps that weren't requested. -5. Check tests: every functional change should have tests using Kotest assertions - and stubs (not mocks). -6. Check the version gate: - - If the repository has a root `version.gradle.kts`, confirm it was - incremented when the change is user-visible. - - If root `version.gradle.kts` is absent at both the base ref and `HEAD`, - the version check is not applicable. Do not report a missing version bump - or ask for the file to be created. - -## Output format - -Return three sections, in this order: - -- **Must fix** โ€” violations of safety rules, broken builds, missing version - bump when the version gate applies, missing tests for functional changes. -- **Should fix** โ€” coding-guideline violations and clearer idiomatic alternatives. - Cite the specific guideline. -- **Nits** โ€” style and naming suggestions. - -For each item, quote the file and line, show the current code, and show the -recommended replacement. If there's nothing in a section, write "None." - -End with a one-line verdict: `APPROVE`, `APPROVE WITH CHANGES`, or `REQUEST CHANGES`. diff --git a/.agents/skills/move-files/SKILL.md b/.agents/skills/move-files/SKILL.md deleted file mode 100644 index b92b05d3f5..0000000000 --- a/.agents/skills/move-files/SKILL.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -name: move-files -description: > - Move or rename any files/directories in a repo: preserve history, update all - references and build metadata, verify no stale paths remain. ---- - -# Move Files - -## Workflow - -1. Preflight. - - Run `git status --short`. - - Map each `source -> destination`. - - Classify scope: simple same-module moves stay targeted; package, module, or - cross-module moves need broader inspection. - - Ask before ambiguous mappings, destination conflicts, or unclear semantic - package/module changes. - -2. Search before moving. - - Search all old identifiers: paths, names, resource refs, doc links. - - For Gradle/module/source-set moves, check `settings.gradle.kts`, - `build.gradle.kts`, and `buildSrc`. - - For Kotlin/Java, update package declarations only when package intent - changes. - -3. Move safely. - - Always use `git mv` for tracked files in the repo. If sandboxing blocks - it, request approval; do not use delete/create as a fallback. - - Use filesystem moves only for untracked/generated/out-of-git files. - - Create parent directories first. - - For case-only renames, move through a temporary name. - -4. Repair references. - - Update all references: imports, build metadata, docs, resources, and scripts. - - Start search scope narrow: affected directory, then module, then repo-wide. - - Prefer precise edits; avoid broad replacements on generic names. - -5. Verify. - - Re-run targeted searches for old tokens. - - Run `git status --short` and confirm the delta matches the move. - - Run focused validation for moved files, or state what could not run. - -6. Ensure the version is bumped. - Invoke `/version-bumped` so the branch carries a strictly greater - `version.gradle.kts` than the base ref before any `./gradlew build` - (which can transitively `publishToMavenLocal` and overwrite - consumer-facing snapshots). The skill is a no-op if a bump already - happened earlier on the branch. - -## Repo Notes - -Follow `.agents/project-structure-expectations.md` for module/source-set/test moves. - -## Report - -Return: `Moved[]`, `UpdatedRefs[]`, `Verification[]`, `Risks[]`. diff --git a/.agents/skills/move-files/agents/openai.yaml b/.agents/skills/move-files/agents/openai.yaml deleted file mode 100644 index ba90a9f8f2..0000000000 --- a/.agents/skills/move-files/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Move Files" - short_description: "Move files safely across a repo" - default_prompt: "Use $move-files to relocate files or directories in this repository while preserving history, updating references, and verifying the result." diff --git a/.agents/skills/pre-pr/SKILL.md b/.agents/skills/pre-pr/SKILL.md deleted file mode 100644 index 591b26b308..0000000000 --- a/.agents/skills/pre-pr/SKILL.md +++ /dev/null @@ -1,188 +0,0 @@ ---- -name: pre-pr -description: > - Run the pre-PR checklist for this repo: apply the version gate only when - the repository has a root `version.gradle.kts`, run a scope-dependent - build/check command per `.agents/running-builds.md` (docs-only โ†’ `dokka`; - code/deps โ†’ `build`; proto โ†’ `clean build`; no documented command โ†’ skipped), - and invoke the relevant reviewers (`kotlin-review`, `review-docs`, - `dependency-audit`, - `check-links`) against the branch diff. On success, write a sentinel file at - `.git/pre-pr.ok` so the `gh pr create` hook can verify the checklist ran - for the current HEAD. Use before opening a PR, or when CI rejected a - branch and you want a fast local repro. ---- - -# Pre-PR checklist (repo-specific) - -You are the pre-PR gate for this repository. You compose the existing -reviewers and the documented repository rules into a single pass that must -succeed before a pull request is opened. - -This skill supports both versioned Gradle Build Tools projects and repositories -that intentionally do not have `version.gradle.kts`. Do not create -`version.gradle.kts` just to satisfy this checklist. When the file is absent -from the project root, the version-bump check is **not applicable**. - -The authoritative standards live in `.agents/`: - -- `.agents/version-policy.md` โ€” applies only when the repository has a root - `version.gradle.kts`. -- `.agents/running-builds.md` โ€” which build/check command to run. -- `.agents/safety-rules.md` and `.agents/advanced-safety-rules.md` โ€” hard - constraints checked by the reviewers. - -## Procedure - -Run steps 1โ€“4 fully before aggregating. Collect all findings; do not stop at -the first failure. - -### 1. Determine scope and repository capabilities - -- Base ref: `master` unless the user provides a different one. -- Changed files: `git diff ...HEAD --name-only` - Remove any path matching the config-distributed list in - `AGENTS.md ยง Code review`. A PR that contains *only* config-distributed - files needs no build, no reviewers, and should PASS immediately โ€” skip - to step 6 with `build=skipped`, `build_status=skipped`, - `reviewers=none`, `version=not-applicable`. -- Repository root: `git rev-parse --show-toplevel` -- Version gate: check only the repository-root `version.gradle.kts`. - - Absent at both sides โ†’ `not-applicable`, continue. - - Present at `HEAD` โ†’ enforce in step 2. - - Present at `` but missing at `HEAD` โ†’ fail unless the user - explicitly asked to migrate away from Gradle Build Tools versioning. -- Classify changes: - - **proto** โ€” any `*.proto` changed - - **code** โ€” any `*.kt`, `*.kts`, or `*.java` changed - - **docs** โ€” any `*.md` or doc-only source edits changed - - **deps** โ€” any file under `buildSrc/src/main/kotlin/io/spine/dependency/` changed - - **site** โ€” any file under `docs/**` or `lychee.toml` (triggers Hugo link - check; pure `README.md` or KDoc-only changes do *not* count) - -### 2. Version-bump check - -- Skip when version gate is `not-applicable`. -- Read `version.gradle.kts` at `HEAD`. Read `` only if the file exists - there; if it does not, the file is newly introduced โ€” record the introduced - version and continue. -- When both sides have the file: if the version is not strictly greater (semver - + Spine snapshot rules in `.agents/version-policy.md`): if - `.agents/skills/bump-version/` exists, **auto-fix immediately** by invoking - `/bump-version` without asking; otherwise record a Must-fix and continue. - Re-read the file after the fix. If the version is still not strictly greater, - record a Must-fix and continue. If the auto-fix succeeded, recompute the - changed-file list (`git diff ...HEAD --name-only`) before proceeding to - Step 3 โ€” the bump commit adds `version.gradle.kts` to the diff. - -### 3. Build or check - -Pick the target per `.agents/running-builds.md`: - -- **proto** changed โ†’ `./gradlew clean build` -- Else **code** changed โ†’ `./gradlew build` -- Else **docs**-only โ†’ `./gradlew dokka` - -If `./gradlew` is absent, read `.agents/running-builds.md` for the -repository-specific command. If that file is also absent, or if none is -documented for the change type, record `build_status=skipped` with the -reason and continue. - -Run the chosen command. On failure, record the first failing task and -continue to step 4 โ€” do not abort. Pass `build_status=FAIL` in the context -given to reviewers so they can discount false positives from non-compiling -code. - -### 4. Reviewers (run in parallel) - -Dispatch relevant reviewers concurrently; collect all verdicts before -aggregating. Before dispatching, check that the skill directory exists under -`.agents/skills/`; if a skill is absent, skip it with a note "not applicable -for this repo" rather than failing. - -- **code** changed โ†’ `kotlin-review` -- **docs** or KDoc changed โ†’ `review-docs` -- **deps** changed โ†’ `dependency-audit` -- **site** changed โ†’ `check-links` (unless the sentinel short-circuit below - applies) - -**`check-links` sentinel short-circuit.** Read `.git/check-links.ok` (if -present). If `head=` equals the current **full** HEAD SHA and `status=PASS`, skip -dispatch and record `APPROVE` with note "cached from `.git/check-links.ok`" -(caching its ~30 s rebuild+serve cycle; the result is deterministic for a given -HEAD). Otherwise dispatch normally. - -Pass each reviewer: base ref, changed-file list, build result, version result. -When the version check is `not-applicable`, say so explicitly so reviewers don't flag a -missing version bump. - -**Auto-fix policy for reviewer findings:** - -- Findings from `kotlin-review`, `review-docs`, or `dependency-audit` โ†’ record - as Must-fix or Should-fix; do **not** auto-apply. Surface them and wait for - user action. -- If a reviewer reports a missing version bump after Step 2 already ran, the - auto-fix did not take โ€” record a Must-fix and do not silently re-apply. -- `dependency-audit` reports a **version rollback** โ†’ do **not** auto-fix. - Surface it as a Must-fix and wait for user confirmation, because a rollback - can be intentional. - -### 5. Aggregate - -- **PASS**: version check passed or `not-applicable`, build succeeded or - `build_status=skipped` (no documented command for the change type), every - reviewer returned `APPROVE` or `APPROVE WITH CHANGES`, and no unaddressed - Must-fix items remain. -- **FAIL**: anything else. - -### 6. Sentinel - -Write `.git/pre-pr.ok` at the repo root (never under `.claude/`). The `gh pr -create` hook (`.agents/scripts/pre-pr-gate.sh`) checks `head=` and `status=`; -field names in this block are part of that contract. - -``` -head= -branch= -status=PASS|FAIL -timestamp= -build= -build_status=PASS|FAIL|skipped -reviewers= -version=new, introduced:, or "not-applicable"> -``` - -## Output format - -**On PASS** โ€” single line: - -``` -Pre-PR: PASS ( vs ) โ€” ready to `gh pr create`. -``` - -**On FAIL** โ€” header line, then only the items that need attention, each -prefixed with the source reviewer or check: - -``` -Pre-PR: FAIL ( vs ) - -Must fix: -- [kotlin-review] -- [review-docs] - -Should fix: -- [dependency-audit] -``` - -Report nothing about checks that passed. If auto-fixes were applied, list -them in one line before the verdict: `Auto-fixed: .` - -## Notes - -- This skill must NOT create the PR itself. -- This skill must NOT create `version.gradle.kts`. -- The sentinel lives under `.git/` โ€” per-clone, never committed. -- Each reviewer is the source of truth for its own checks; this skill only - orchestrates and aggregates. -- This skill may auto-fix a missing version bump by invoking `/bump-version`; - all other fixes require explicit user confirmation. diff --git a/.agents/skills/review-docs/SKILL.md b/.agents/skills/review-docs/SKILL.md deleted file mode 100644 index d7cac33237..0000000000 --- a/.agents/skills/review-docs/SKILL.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -name: review-docs -description: > - Review documentation changes โ€” KDoc/Javadoc inside Kotlin/Java sources and - Markdown docs (`README.md`, `docs/**`) โ€” against Spine documentation - conventions. Use when a diff touches doc comments or Markdown, before - opening a doc-affecting PR, or when asked for a documentation review. - Read-only; does not run builds. ---- - -# Review documentation (repo-specific) - -You are the documentation reviewer for a Spine Event Engine project. You -focus strictly on documentation quality โ€” prose, KDoc/Javadoc, and Markdown โ€” -and deliberately do **not** duplicate the code-review skill (which owns -Kotlin idioms, safety rules, tests, and version-gate checks). - -The authoritative standards live in `.agents/`: - -- `.agents/documentation-guidelines.md` โ€” commenting rules, TODO-comment - format, "file/dir names as code", widow/runt/orphan/river rule (with the - diagram at `.agents/widow-runt-orphan.jpg`). -- `.agents/documentation-tasks.md` โ€” KDoc-example requirement on APIs; - Javadoc โ†’ KDoc conversion rules (`

` removal, etc.). -- `.agents/skills/writer/SKILL.md` โ€” Markdown conventions (footnote-style - reference links for external URLs, typographic quotes only on actual - page/section titles, sidenav-sync rules under `docs/`). -- `.agents/running-builds.md` โ€” for doc-only Kotlin/Java changes the right - build is `./gradlew dokka` (no tests required). - -## Review procedure - -1. **Scope the diff.** Obtain the change set via `git diff --staged` or - `git diff ...HEAD` depending on what the user describes. Restrict - to files matching: - - `**/*.kt`, `**/*.kts`, `**/*.java` (for KDoc/Javadoc inside sources) - - `**/*.proto` (for file-level documentation headers) - - `**/*.md` (Markdown docs) - Do **not** review the full repo โ€” only what changed. - Filter out config-distributed files (see `AGENTS.md ยง Code review` for the - exact list) before proceeding. If nothing remains after filtering, return - `APPROVE โ€” all changes are config-distributed files.` and stop. - -2. **Read each affected file fully, not just the hunks.** Prose review - requires surrounding context โ€” judging widows/runts/orphans, link - placement, and KDoc completeness needs the whole paragraph and the - surrounding declarations. - -3. **Stay in scope.** If you spot a code-quality issue (idiom, naming, - tests, version-gate applicability), note it briefly as a "for the code - reviewer" item under Nits โ€” do not expand the review. - -## Checks - -### A. KDoc / Javadoc inside sources - -- **Public and internal APIs carry KDoc.** Per `documentation-tasks.md`, - KDoc should include at least one usage example for non-trivial APIs. - Missing KDoc on a new or modified public/internal symbol is a Should-fix. -- **No Javadoc residue in Kotlin.** When converting from Java: - - `

` tags on a text line removed (`"

This"` โ†’ `"This"`). - - `

` on its own line replaced with a blank line. - - HTML entities (`&`, `<`, โ€ฆ) converted to literals where appropriate. -- **Inline comments in production code are minimized.** Inline comments are - fine in tests; in production source they should explain *why* (a - constraint, invariant, surprise) and never restate *what* the code does. -- **TODO comments follow the Spine format.** Linked from - `documentation-guidelines.md` to the wiki "TODO-comments" page. A bare - `// TODO: โ€ฆ` without owner/issue reference is a Should-fix. -- **File and directory names rendered as code.** Within KDoc/Javadoc prose, - `path/to/file.kt` and `module-name` must use backticks. -- **Multi-paragraph Protobuf headers end with an empty comment line.** In - `.proto` files, if the file-level documentation header has more than one - paragraph, it must end with a trailing empty comment line (`//`). - -### B. Markdown docs - -- **Footnote-style reference links** for external `https://` URLs (per the - `writer` skill). Inline `[label](https://โ€ฆ)` in body prose is a - Should-fix; inline links to local relative paths are fine. -- **Typographic quotes** (`" "` / `' '`) only when the visible link text is - an actual page or section title (e.g., the "Getting started" page). - Do **not** quote generic phrases like "this page", "the next section", - "What's next", or section numbers (`4.3`). -- **Sidenav sync.** If the diff adds/removes/renames/moves a page under - `docs/content/docs/

/`, the matching current-version - `sidenav.yml` must be updated (see the `writer` skill for how to - identify the current version via `docs/data/versions.yml`). A missing - sidenav update is a Must-fix. -- **Fenced code blocks** for commands and examples โ€” no indented code - blocks for shell snippets (they swallow `$` prompts and hurt copy/paste). -- **Heading hierarchy.** No skipped levels (`#` โ†’ `###`); exactly one `#` - per file. - -### C. Prose flow (Spine-specific) - -- **Avoid widows, runts, orphans, and rivers** โ€” the rule from - `documentation-guidelines.md` with the diagram at - `.agents/widow-runt-orphan.jpg`. Operationally: - - **Widow / runt**: a paragraph's last line containing only one short - word (or a hyphenated fragment). Reflow the prior line. - - **Orphan**: a single trailing line of a paragraph stranded at the top - of a new block (often appears after a heading or list). Reflow. - - **River**: a vertical "gap" of aligned spaces running down justified - text. Rare in Markdown but possible in tables โ€” reflow the table or - rewrite to break the alignment. - Quote the offending paragraph and propose a rewording that fixes it. - -### D. Terminology and tone - -- **Match code identifiers verbatim.** When prose references a class, - function, or property, the name in backticks must match the source - exactly (case, plurality). -- **Consistent terminology across the diff.** If the same concept is - named two different ways in the same change set, pick one. - -## Output format - -Three sections, in this order: - -- **Must fix** โ€” broken/missing KDoc on a newly-introduced public API, - missing sidenav sync, broken cross-references, Javadoc residue - (`

` tags) left in Kotlin KDoc, broken Markdown links. -- **Should fix** โ€” TODO format, inline-comment overuse in production, - inline external links that should be footnote-style, missing typographic - quotes (or unwanted ones), widow/runt/orphan/river paragraphs, - fenced-vs-indented code blocks. -- **Nits** โ€” wording, terminology drift, code-identifier capitalization - in prose, "for the code reviewer" pointers if any code issues surfaced - incidentally. - -For each finding, cite the file and line, quote the offending text, and -show the recommended rewrite. If a section is empty, write "None." - -End with a one-line verdict: `APPROVE`, `APPROVE WITH CHANGES`, or -`REQUEST CHANGES`. diff --git a/.agents/skills/update-copyright/SKILL.md b/.agents/skills/update-copyright/SKILL.md deleted file mode 100644 index 6afc4c7cf2..0000000000 --- a/.agents/skills/update-copyright/SKILL.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: update-copyright -description: > - Update source file copyright headers from the IntelliJ IDEA copyright profile, - replacing `today.year` with the current year. - Automatically apply when source files are modified in a change set. ---- - -# Copyright Update - -**Command:** `python3 .agents/skills/update-copyright/scripts/update_copyright.py` - -1. Scope: explicit files/dirs from the user, or all tracked source files if none given. -2. No explicit paths โ†’ run with `--dry-run` first, then without. -3. Relay stdout (notice source, file count, changed paths) to the user. -4. Never add a copyright header to a file that does not already have one. diff --git a/.agents/skills/update-copyright/agents/openai.yaml b/.agents/skills/update-copyright/agents/openai.yaml deleted file mode 100644 index 246dd647f7..0000000000 --- a/.agents/skills/update-copyright/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Copyright Update" - short_description: "Refresh source copyright headers" - default_prompt: "Use $update-copyright to refresh source file copyright headers from the IntelliJ IDEA copyright profile in this repository." diff --git a/.agents/skills/update-copyright/scripts/update_copyright.py b/.agents/skills/update-copyright/scripts/update_copyright.py deleted file mode 100755 index 2dbf8bbc48..0000000000 --- a/.agents/skills/update-copyright/scripts/update_copyright.py +++ /dev/null @@ -1,389 +0,0 @@ -#!/usr/bin/env python3 -"""Update source copyright headers from IntelliJ IDEA copyright profiles.""" - -from __future__ import annotations - -import argparse -import datetime as dt -import html -import re -import subprocess -import sys -from pathlib import Path -from xml.etree import ElementTree as ET - - -BLOCK_EXTENSIONS = { - ".c", - ".cc", - ".cpp", - ".cs", - ".css", - ".cxx", - ".dart", - ".go", - ".gradle", - ".groovy", - ".h", - ".hh", - ".hpp", - ".java", - ".js", - ".jsx", - ".kt", - ".kts", - ".less", - ".m", - ".mm", - ".proto", - ".rs", - ".scala", - ".scss", - ".swift", - ".ts", - ".tsx", -} -HASH_EXTENSIONS = { - ".bash", - ".bzl", - ".properties", - ".pl", - ".py", - ".rb", - ".sh", - ".toml", - ".yaml", - ".yml", - ".zsh", -} -XML_EXTENSIONS = { - ".fxml", - ".pom", - ".wsdl", - ".xml", - ".xsd", - ".xsl", - ".xslt", -} -EXCLUDED_DIRS = { - ".agents", - ".git", - ".gradle", - ".idea", - ".kotlin", - "build", - "generated", - "out", - "tmp", -} -EXCLUDED_FILES = { - "gradlew", - "gradlew.bat", -} - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser( - description=( - "Update source copyright headers from " - ".idea/copyright/profiles_settings.xml." - ) - ) - parser.add_argument( - "paths", - nargs="*", - help="Files or directories to update. Defaults to tracked source files.", - ) - parser.add_argument( - "--root", - type=Path, - default=Path.cwd(), - help="Repository root. Defaults to the current working directory.", - ) - parser.add_argument( - "--year", - default=str(dt.date.today().year), - help="Year to substitute for today.year. Defaults to the current year.", - ) - parser.add_argument( - "--dry-run", - action="store_true", - help="Report files that would change without writing them.", - ) - parser.add_argument( - "--check", - action="store_true", - help="Exit with status 1 if any file would change; do not write files.", - ) - return parser.parse_args() - - -def profile_filename(profile_name: str) -> str: - stem = re.sub(r"[^A-Za-z0-9]+", "_", profile_name).strip("_") - if not stem: - raise ValueError("The default copyright profile name is empty.") - return f"{stem}.xml" - - -def load_notice(root: Path, year: str) -> tuple[str, Path]: - settings_path = root / ".idea" / "copyright" / "profiles_settings.xml" - if not settings_path.is_file(): - raise FileNotFoundError(f"Missing {settings_path}") - - settings_root = ET.parse(settings_path).getroot() - settings = settings_root.find(".//settings") - if settings is None: - raise ValueError(f"{settings_path} does not contain a settings tag.") - - default_profile = settings.get("default") - if not default_profile: - raise ValueError(f"{settings_path} settings tag has no default attribute.") - - profile_path = settings_path.parent / profile_filename(default_profile) - if not profile_path.is_file(): - raise FileNotFoundError( - f"Default profile {default_profile!r} resolves to missing {profile_path}" - ) - - profile_root = ET.parse(profile_path).getroot() - notice = None - for option in profile_root.findall(".//option"): - if option.get("name") == "notice": - notice = option.get("value") - break - if notice is None: - raise ValueError(f"{profile_path} has no option named 'notice'.") - - decoded = html.unescape(notice) - decoded = decoded.replace("${today.year}", year) - decoded = decoded.replace("$today.year", year) - decoded = decoded.replace("today.year", year) - return decoded.rstrip(), profile_path - - -def style_for(path: Path) -> str | None: - name = path.name - suffix = path.suffix.lower() - if name.endswith((".sh.template", ".bash.template", ".zsh.template")): - return "hash" - if suffix in BLOCK_EXTENSIONS: - return "block" - if suffix in HASH_EXTENSIONS: - return "hash" - if suffix in XML_EXTENSIONS: - return "xml" - return None - - -def is_excluded(path: Path) -> bool: - if path.name in EXCLUDED_FILES: - return True - parts = path.parts - if len(parts) >= 2 and parts[0] == "gradle" and parts[1] == "wrapper": - return True - return any(part in EXCLUDED_DIRS for part in parts) - - -def tracked_files(root: Path) -> list[Path]: - try: - result = subprocess.run( - ["git", "-C", str(root), "ls-files", "-z"], - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - except (FileNotFoundError, subprocess.CalledProcessError): - return [ - path.relative_to(root) - for path in root.rglob("*") - if path.is_file() and not is_excluded(path.relative_to(root)) - ] - - paths = [] - for item in result.stdout.decode("utf-8").split("\0"): - if not item: - continue - path = Path(item) - if (root / path).is_file(): - paths.append(path) - return paths - - -def expand_requested_paths(root: Path, requested: list[str]) -> list[Path]: - if not requested: - paths = tracked_files(root) - else: - paths = [] - for item in requested: - path = (root / item).resolve() - if not path.exists(): - raise FileNotFoundError(f"Path does not exist: {item}") - if not path.is_relative_to(root): - raise ValueError( - f"Path is outside the repository root: {item!r} " - f"(resolved to {path}, root is {root})" - ) - if path.is_dir(): - for child in path.rglob("*"): - if child.is_file(): - paths.append(child.relative_to(root)) - else: - paths.append(path.relative_to(root)) - - unique = sorted(set(paths), key=lambda p: p.as_posix()) - return [ - path - for path in unique - if style_for(path) is not None and not is_excluded(path) - ] - - -def newline_for(text: str) -> str: - return "\r\n" if "\r\n" in text else "\n" - - -def build_header(notice: str, style: str, newline: str) -> str: - lines = notice.splitlines() - if style == "block": - body = newline.join(f" * {line}" if line else " *" for line in lines) - return f"/*{newline}{body}{newline} */{newline}{newline}" - if style == "hash": - body = newline.join(f"# {line}" if line else "#" for line in lines) - return f"{body}{newline}{newline}" - if style == "xml": - body = newline.join(f" ~ {line}" if line else " ~" for line in lines) - return f"{newline}{newline}" - raise ValueError(f"Unsupported comment style: {style}") - - -def split_leading_directive(text: str, style: str, newline: str) -> tuple[str, str]: - if style == "hash" and text.startswith("#!"): - line_end = text.find("\n") - if line_end == -1: - return text + newline + newline, "" - prefix = text[: line_end + 1] + newline - return prefix, strip_leading_blank_lines(text[line_end + 1 :]) - - if style == "xml" and text.startswith("") - if close != -1: - line_end = text.find("\n", close) - if line_end == -1: - return text + newline + newline, "" - prefix = text[: line_end + 1] + newline - return prefix, strip_leading_blank_lines(text[line_end + 1 :]) - - return "", strip_leading_blank_lines(text) - - -def strip_leading_blank_lines(text: str) -> str: - return re.sub(r"^(?:[ \t]*\r?\n)+", "", text) - - -def strip_existing_header(text: str, style: str) -> tuple[str, bool]: - if style == "block" and text.startswith("/*"): - close = text.find("*/") - if close != -1: - candidate = text[: close + 2] - if is_copyright_header(candidate): - return strip_leading_blank_lines(text[close + 2 :]), True - - if style == "xml" and text.startswith("") - if close != -1: - candidate = text[: close + 3] - if is_copyright_header(candidate): - return strip_leading_blank_lines(text[close + 3 :]), True - - if style == "hash": - lines = text.splitlines(keepends=True) - end = 0 - for line in lines: - stripped = line.strip() - if stripped == "" or stripped.startswith("#"): - end += len(line) - continue - break - candidate = text[:end] - if candidate and is_copyright_header(candidate): - return strip_leading_blank_lines(text[end:]), True - - return text, False - - -def is_copyright_header(text: str) -> bool: - limited = text[:5000] - return "Copyright" in limited and ( - "Licensed under" in limited or "All rights reserved" in limited - ) - - -def updated_text(text: str, notice: str, style: str) -> str: - original = text - bom = "\ufeff" if text.startswith("\ufeff") else "" - if bom: - text = text[1:] - newline = newline_for(text) - prefix, body = split_leading_directive(text, style, newline) - body, had_header = strip_existing_header(body, style) - if not had_header: - return original - return bom + prefix + build_header(notice, style, newline) + body - - -def update_file(root: Path, path: Path, notice: str, dry_run: bool) -> bool: - absolute = root / path - style = style_for(path) - if style is None: - return False - - try: - text = absolute.read_text(encoding="utf-8") - except FileNotFoundError: - print(f"Skipping missing file: {path}", file=sys.stderr) - return False - except UnicodeDecodeError: - print(f"Skipping non-UTF-8 file: {path}", file=sys.stderr) - return False - - next_text = updated_text(text, notice, style) - if next_text == text: - return False - - if not dry_run: - with absolute.open("w", encoding="utf-8", newline="") as file: - file.write(next_text) - return True - - -def main() -> int: - args = parse_args() - root = args.root.resolve() - notice, profile_path = load_notice(root, args.year) - try: - paths = expand_requested_paths(root, args.paths) - except (FileNotFoundError, ValueError) as exc: - print(f"error: {exc}", file=sys.stderr) - return 2 - dry_run = args.dry_run or args.check - - changed = [ - path - for path in paths - if update_file(root, path, notice, dry_run=dry_run) - ] - - rel_profile = profile_path.relative_to(root) - action = "Would update" if dry_run else "Updated" - print(f"Notice source: {rel_profile}") - print(f"{action} {len(changed)} file(s).") - for path in changed: - print(path.as_posix()) - - if args.check and changed: - return 1 - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/.agents/skills/update-copyright/tests/test_update_copyright.py b/.agents/skills/update-copyright/tests/test_update_copyright.py deleted file mode 100644 index 8770b3275e..0000000000 --- a/.agents/skills/update-copyright/tests/test_update_copyright.py +++ /dev/null @@ -1,130 +0,0 @@ -from __future__ import annotations - -import subprocess -import sys -import tempfile -import unittest -from pathlib import Path - - -SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "update_copyright.py" - - -class UpdateCopyrightTest(unittest.TestCase): - def test_default_run_leaves_plain_source_without_header_unchanged(self) -> None: - with tempfile.TemporaryDirectory() as temp_dir: - root = Path(temp_dir) - self.write_profile(root) - source = root / "Foo.java" - original = "class Foo {}\n" - source.write_text(original, encoding="utf-8") - - subprocess.run(["git", "init", "-q"], cwd=root, check=True) - subprocess.run(["git", "add", "Foo.java"], cwd=root, check=True) - - result = self.run_script(root) - - self.assertEqual(result.returncode, 0, result.stderr) - self.assertIn("Updated 0 file(s).", result.stdout) - self.assertEqual(result.stderr, "") - self.assertEqual(source.read_text(encoding="utf-8"), original) - - def test_existing_header_is_updated(self) -> None: - with tempfile.TemporaryDirectory() as temp_dir: - root = Path(temp_dir) - self.write_profile(root) - source = root / "Foo.java" - source.write_text( - "/*\n" - " * Copyright 2024 ACME\n" - " * All rights reserved\n" - " */\n" - "\n" - "class Foo {}\n", - encoding="utf-8", - ) - - result = self.run_script(root, "--year", "2026", "Foo.java") - - self.assertEqual(result.returncode, 0, result.stderr) - self.assertIn("Updated 1 file(s).", result.stdout) - self.assertIn("Foo.java", result.stdout) - self.assertEqual(result.stderr, "") - self.assertEqual( - source.read_text(encoding="utf-8"), - "/*\n" - " * Copyright 2026 ACME\n" - " * All rights reserved\n" - " */\n" - "\n" - "class Foo {}\n", - ) - - def test_default_run_skips_tracked_files_deleted_from_working_tree(self) -> None: - with tempfile.TemporaryDirectory() as temp_dir: - root = Path(temp_dir) - self.write_profile(root) - source = root / "Foo.java" - source.write_text("class Foo {}\n", encoding="utf-8") - - subprocess.run(["git", "init", "-q"], cwd=root, check=True) - subprocess.run(["git", "add", "Foo.java"], cwd=root, check=True) - source.unlink() - - result = subprocess.run( - [ - sys.executable, - str(SCRIPT), - "--root", - str(root), - "--dry-run", - ], - check=False, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - - self.assertEqual(result.returncode, 0, result.stderr) - self.assertIn("Would update 0 file(s).", result.stdout) - self.assertEqual(result.stderr, "") - - @staticmethod - def run_script(root: Path, *args: str) -> subprocess.CompletedProcess[str]: - return subprocess.run( - [ - sys.executable, - str(SCRIPT), - "--root", - str(root), - *args, - ], - check=False, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - - @staticmethod - def write_profile(root: Path) -> None: - copyright_dir = root / ".idea" / "copyright" - copyright_dir.mkdir(parents=True) - (copyright_dir / "profiles_settings.xml").write_text( - '' - '' - "\n", - encoding="utf-8", - ) - (copyright_dir / "Default.xml").write_text( - '' - "" - '" - "\n", - encoding="utf-8", - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/.agents/skills/version-bumped/SKILL.md b/.agents/skills/version-bumped/SKILL.md deleted file mode 100644 index 86ca53df04..0000000000 --- a/.agents/skills/version-bumped/SKILL.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -name: version-bumped -description: > - Verify the current branch has bumped `version.gradle.kts` strictly above - the base ref; invoke `/bump-version` to auto-recover if not. Composable: - other modifying skills (`dependency-update`, `bump-gradle`, - `java-to-kotlin`, `move-files`) call this as their final step so a - `./gradlew build` or `publishToMavenLocal` can never overwrite a - previously published Maven Local artifact that integration tests in - consumer repos depend on. ---- - -# Ensure version is bumped - -This skill is the agent-facing wrapper around -`.agents/skills/version-bumped/scripts/version-bumped.sh`. The script is the source of truth for -"has this branch advanced the version vs base?"; this skill just runs it -and, if it fails, invokes `/bump-version` and re-runs to confirm. - -The same logic is enforced as a hook -(`.agents/scripts/publish-version-gate.sh`) that fires before any -`./gradlew โ€ฆ (build|publish|publishToMavenLocal)` invocation, so even -direct gradle calls cannot bypass it. This skill exists for the -cooperative path โ€” other skills calling it before they finish, so the -user is never surprised by a blocked gradle command later. - -The premise is simple: any feature branch is a candidate for publishing, -even when the only change is the version bump itself (sometimes the bump -is the entire change, used to retry a publish that failed because Maven -repositories were overloaded). So if the branch differs from base at all, -the version must advance. - -## When to use - -- Automatically: as the final step of any skill that may change files on - the branch. -- Manually (`/version-bumped`): before running `./gradlew build` or - `./gradlew publishToMavenLocal` on a feature branch when you are not - sure whether the version has already been bumped. - -## Procedure - -1. Run the deterministic check: - - ```bash - .agents/skills/version-bumped/scripts/version-bumped.sh - ``` - - Honor `VERSION_BUMPED_BASE` if the user has set a non-default base ref - (e.g. `origin/master`, or a release branch). - -2. Interpret the exit code: - - - **0** โ€” Done. Either the repository has no root `version.gradle.kts` - (the version check is `N/A`), the branch has no diff vs base, or the - version is already strictly greater. Report a one-line confirmation - and stop. - - **1** โ€” Block. The script's stderr explains which check failed. - Proceed to step 3. - - **2** โ€” Configuration error (no merge-base, parse failure on - `version.gradle.kts`). Do **not** invoke `/bump-version` - automatically. Surface the script's stderr to the user and stop. - -3. On exit 1, invoke `/bump-version` to perform the actual bump. That - skill owns the policy (snapshot numbering, the commit subject, the - rebuild, dependency-report regeneration, and the conflict rule). Do - not duplicate its work here. - -4. After `/bump-version` finishes, re-run the deterministic check. If it - now passes, report the new version on the branch. If it still fails, - surface the stderr unchanged and stop โ€” do not loop. - -## Why this skill is separate from `/bump-version` - -`/bump-version` is the **action** (it edits `version.gradle.kts`, -commits, rebuilds, may commit reports). `/version-bumped` is the -**guard** (read-only check, optional auto-recovery). Skills that want to -say "make sure the branch has a bumped version" should call -`/version-bumped`, not `/bump-version`, because the guard is a no-op when -the bump is already done โ€” calling `/bump-version` unconditionally would -double-bump on every chained skill invocation. - -## Relationship to `checkVersionIncrement` - -The Gradle task `checkVersionIncrement` (in `buildSrc/.../publish/`) -asks a different question: *"is this version already in the remote -Maven metadata?"* It runs on GitHub Actions feature-branch pushes and -fetches the Spine SDK Artifact Registry. The two checks are -complementary โ€” neither subsumes the other. - -## See also - -- `.agents/version-policy.md` โ€” when the version gate applies. -- `.agents/skills/bump-version/SKILL.md` โ€” the bump procedure itself. -- `.agents/skills/pre-pr/SKILL.md` โ€” uses the same check at PR time - (step 2). -- `.agents/skills/version-bumped/scripts/version-bumped.sh` โ€” the deterministic check. -- `.agents/scripts/publish-version-gate.sh` โ€” the hook that enforces the - rule on `./gradlew` invocations. diff --git a/.agents/skills/version-bumped/scripts/version-bumped.sh b/.agents/skills/version-bumped/scripts/version-bumped.sh deleted file mode 100755 index f050a5b79d..0000000000 --- a/.agents/skills/version-bumped/scripts/version-bumped.sh +++ /dev/null @@ -1,276 +0,0 @@ -#!/usr/bin/env bash -# -# Verifies that a feature branch which differs from the base ref also -# bumps `version.gradle.kts` strictly above the base version. Mirrors the -# universal "every branch advances the version" policy: a branch with any -# changes is a candidate for publishing โ€” sometimes the only change is the -# bump itself, used to retry a publish that failed because Maven -# repositories were overloaded. -# -# Exit codes: -# 0 โ€” OK: repo has no root `version.gradle.kts`, OR branch has no diff -# vs base, OR working-tree version is strictly greater than base -# version. -# 1 โ€” Block: branch differs from base but version is unchanged or -# decreased. Stderr points to `/bump-version`. -# 2 โ€” Configuration error (bad base ref, parse failure). Stderr explains. -# -# Inputs (env, all optional): -# VERSION_BUMPED_BASE Base ref to compare against. Default: master, -# then main if master is absent. -# VERSION_BUMPED_KEY Name of the `extra` property holding the -# publishing version (e.g. `versionToPublish`, -# `validationVersion`, `bootstrapVersion`). When -# set, bypasses auto-discovery. Useful for repos -# that don't follow the `version = extra["โ€ฆ"]` -# pattern in `build.gradle.kts`. -# VERSION_BUMPED_QUIET When `1`, suppress the "OK" line on stdout. -# The publish-version-gate hook sets this. -# -# Publishing-key discovery: -# The publishing version's variable name varies across Spine repos -# (`versionToPublish`, `validationVersion`, `compilerVersion`, โ€ฆ). -# `version.gradle.kts` may also declare other `val xxxVersion by extra(...)` entries -# that are *dependency* versions of other Spine modules โ€” not this -# project's own publishing version โ€” so the key cannot be picked by -# inspecting `version.gradle.kts` alone. -# -# The canonical source is `build.gradle.kts`, which assigns -# `version = extra["KEY"]!!`. This script scans for that pattern, -# picks the unique key, and parses its value from `version.gradle.kts`. -# If `build.gradle.kts` does not contain such a line, the script falls -# back to `versionToPublish`. Set `VERSION_BUMPED_KEY` to override. -# -# Notes: -# * Companion to the Gradle task `checkVersionIncrement` (see -# `buildSrc/.../publish/CheckVersionIncrement.kt`). The Gradle task -# asks "is this version already in remote Maven metadata?" โ€” this -# script asks the simpler local question "has this branch advanced -# the version vs base?". The two checks are complementary; neither -# subsumes the other. -# * The working tree is included in the change-detection so the gate -# reflects what `./gradlew build` would actually publish. -# -set -eu - -repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || { - echo "version-bumped: not inside a git repository" >&2 - exit 2 -} -cd "$repo_root" - -version_file="version.gradle.kts" - -# --- N/A: not a versioned project ---------------------------------------- -if [ ! -f "$version_file" ]; then - [ "${VERSION_BUMPED_QUIET:-0}" = "1" ] || echo "version-bumped: N/A (no root version.gradle.kts)" - exit 0 -fi - -# --- Resolve base ref ---------------------------------------------------- -base="${VERSION_BUMPED_BASE:-}" -if [ -z "$base" ]; then - if git show-ref --verify --quiet refs/heads/master; then - base=master - elif git show-ref --verify --quiet refs/heads/main; then - base=main - else - echo "version-bumped: no master or main branch found; set VERSION_BUMPED_BASE" >&2 - exit 2 - fi -fi - -if ! git rev-parse --verify --quiet "$base" >/dev/null; then - echo "version-bumped: base ref '$base' does not resolve" >&2 - exit 2 -fi - -# When we are on the base branch itself, there is nothing to gate. -current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "") -if [ "$current_branch" = "$base" ] || [ "$current_branch" = "${base##*/}" ]; then - [ "${VERSION_BUMPED_QUIET:-0}" = "1" ] || echo "version-bumped: on base branch ($current_branch); nothing to gate" - exit 0 -fi - -merge_base=$(git merge-base HEAD "$base" 2>/dev/null) || { - echo "version-bumped: cannot find merge-base of HEAD and '$base'" >&2 - exit 2 -} - -# --- Detect any branch divergence vs base (committed/worktree/untracked) - -committed=$(git diff --name-only "$merge_base"..HEAD 2>/dev/null || true) -worktree=$(git diff --name-only HEAD 2>/dev/null || true) -untracked=$(git ls-files --others --exclude-standard 2>/dev/null || true) - -if [ -z "$committed" ] && [ -z "$worktree" ] && [ -z "$untracked" ]; then - [ "${VERSION_BUMPED_QUIET:-0}" = "1" ] || echo "version-bumped: no changes vs $base" - exit 0 -fi - -# --- Discover the publishing-version key --------------------------------- -# Source of truth is `build.gradle.kts` (or `build.gradle`). Two shapes are -# recognised, in order: -# -# a) version = extra["KEY"] -# b) version = IDENTIFIER (with `val IDENTIFIER ... by extra` nearby) -# -# Single or double quotes are accepted in shape (a). If multiple distinct -# keys appear across shapes, the script refuses to guess and asks the user -# to set VERSION_BUMPED_KEY. -# -# Return codes: -# 0 โ€” printed a unique key on stdout -# 1 โ€” no candidates found (caller should fall back) -# 2 โ€” ambiguous; diagnostic already on stderr -discover_key() { - local files keys_a keys_b keys count - files="" - [ -f build.gradle.kts ] && files="build.gradle.kts" - [ -f build.gradle ] && files="$files build.gradle" - [ -z "$files" ] && return 1 - # Shape (a): version = extra["KEY"] - # Anchored to start-of-line (modulo leading whitespace) so that comments - # like `// version = extra["x"]` and identifiers like `fooversion = ...` - # don't produce false matches. - # shellcheck disable=SC2086 - keys_a=$(grep -hE '^[[:space:]]*version[[:space:]]*=[[:space:]]*extra[[:space:]]*\[[[:space:]]*["'"'"'][^"'"'"']+["'"'"']' $files 2>/dev/null \ - | sed -nE 's/.*extra[[:space:]]*\[[[:space:]]*["'"'"']([^"'"'"']+)["'"'"'].*/\1/p') - # Shape (b): version = IDENTIFIER (bare Kotlin identifier, no '[' or '"'). - # Only accept the identifier if the same file also declares - # `val IDENTIFIER[: String]? by extra` โ€” otherwise it's a plain local - # variable (common in Groovy `build.gradle`), not an `extra` property we - # can resolve in `version.gradle.kts`. - local candidates_b cand - # shellcheck disable=SC2086 - candidates_b=$(grep -hE '^[[:space:]]*version[[:space:]]*=[[:space:]]*[A-Za-z_][A-Za-z0-9_]*[[:space:]]*$' $files 2>/dev/null \ - | sed -nE 's/^[[:space:]]*version[[:space:]]*=[[:space:]]*([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*$/\1/p') - keys_b="" - for cand in $candidates_b; do - # shellcheck disable=SC2086 - if grep -hE "^[[:space:]]*val[[:space:]]+${cand}([[:space:]]*:[[:space:]]*String)?[[:space:]]+by[[:space:]]+extra([^A-Za-z0-9_]|\$)" $files >/dev/null 2>&1; then - keys_b="${keys_b}${cand} -" - fi - done - keys=$(printf '%s\n%s' "$keys_a" "$keys_b" | sed '/^$/d' | sort -u) - [ -z "$keys" ] && return 1 - count=$(printf '%s\n' "$keys" | wc -l | tr -d ' ') - if [ "$count" -gt 1 ]; then - { - echo "version-bumped: ambiguous publishing key in build scripts:" - while IFS= read -r k; do printf ' %s\n' "$k"; done <<< "$keys" - echo " Set VERSION_BUMPED_KEY to disambiguate." - } >&2 - return 2 - fi - printf '%s' "$keys" -} - -key="${VERSION_BUMPED_KEY:-}" -if [ -z "$key" ]; then - set +e - key=$(discover_key) - rc=$? - set -e - if [ "$rc" = "2" ]; then - exit 2 - fi - if [ "$rc" != "0" ] || [ -z "$key" ]; then - key="versionToPublish" - fi -fi - -# --- Parse a `val KEY by extra(...)` from a Gradle file content ---------- -# Handles three shapes (per .agents/skills/bump-version/SKILL.md step 2): -# 1. val KEY[: String]? by extra("X") โ€” literal extra -# 2. val SRC[: String]? by extra("X") โ€” alias chain via extra -# val KEY[: String]? by extra(SRC) -# 3. val SRC[: String]? = "X" โ€” alias chain via plain val -# val KEY[: String]? by extra(SRC) -# The key name is parameterized so that any project-specific name works -# (versionToPublish, validationVersion, bootstrapVersion, botVersion, โ€ฆ). -parse_version() { - local content="$1" name="$2" - local v varName - # Shape 1: literal. - v=$(printf '%s' "$content" \ - | grep -E "val[[:space:]]+${name}([[:space:]]*:[[:space:]]*String)?[[:space:]]+by[[:space:]]+extra\(\"" \ - | head -n1 \ - | sed -nE 's/.*extra\("([^"]+)".*/\1/p') - if [ -n "$v" ]; then - printf '%s' "$v" - return 0 - fi - # Shapes 2 & 3: extract the alias source identifier. - varName=$(printf '%s' "$content" \ - | grep -E "val[[:space:]]+${name}([[:space:]]*:[[:space:]]*String)?[[:space:]]+by[[:space:]]+extra\(" \ - | head -n1 \ - | sed -nE 's/.*extra\(([A-Za-z_][A-Za-z0-9_]*)\).*/\1/p') - if [ -n "$varName" ]; then - # Shape 2: source is `val SRC ... by extra("X")`. - v=$(printf '%s' "$content" \ - | grep -E "val[[:space:]]+${varName}([[:space:]]*:[[:space:]]*String)?[[:space:]]+by[[:space:]]+extra\(\"" \ - | head -n1 \ - | sed -nE 's/.*extra\("([^"]+)".*/\1/p') - if [ -n "$v" ]; then - printf '%s' "$v" - return 0 - fi - # Shape 3: source is `val SRC[: String]? = "X"`. - v=$(printf '%s' "$content" \ - | grep -E "val[[:space:]]+${varName}([[:space:]]*:[[:space:]]*String)?[[:space:]]*=[[:space:]]*\"" \ - | head -n1 \ - | sed -nE 's/.*=[[:space:]]*"([^"]+)".*/\1/p') - if [ -n "$v" ]; then - printf '%s' "$v" - return 0 - fi - fi - return 1 -} - -head_content=$(cat "$version_file" 2>/dev/null || true) -head_version=$(parse_version "$head_content" "$key" || true) -if [ -z "$head_version" ]; then - echo "version-bumped: cannot parse '$key' from working-tree $version_file" >&2 - exit 2 -fi - -# Base content may legitimately not exist (file newly introduced). -base_content=$(git show "$base:$version_file" 2>/dev/null || true) -if [ -z "$base_content" ]; then - [ "${VERSION_BUMPED_QUIET:-0}" = "1" ] || echo "version-bumped: $version_file newly introduced at $head_version; treating as bumped" - exit 0 -fi - -base_version=$(parse_version "$base_content" "$key" || true) -if [ -z "$base_version" ]; then - echo "version-bumped: cannot parse '$key' from $base:$version_file" >&2 - exit 2 -fi - -# --- Strict-greater comparison via `sort -V` ----------------------------- -if [ "$head_version" = "$base_version" ]; then - cmp="equal" -elif [ "$(printf '%s\n%s\n' "$base_version" "$head_version" | sort -V | tail -n1)" = "$head_version" ]; then - cmp="greater" -else - cmp="lesser" -fi - -if [ "$cmp" = "greater" ]; then - [ "${VERSION_BUMPED_QUIET:-0}" = "1" ] || echo "version-bumped: OK ($key: $base_version -> $head_version)" - exit 0 -fi - -cat >&2 < - Write, edit, and restructure user-facing and developer-facing documentation. - Use when asked to create/update docs such as `README.md`, `docs/**`, and - other Markdown documentation, including keeping docs navigation data in sync; - when drafting tutorials, guides, troubleshooting pages, or migration notes; and - when improving inline API documentation (KDoc) and examples. ---- - -# Write documentation (repo-specific) - -## Decide the target and audience - -- Identify the target reader: end user, contributor, maintainer, or tooling/automation. -- Identify the task type: new doc, update, restructure, or documentation audit. -- Identify the acceptance criteria: โ€œwhat is correct when the reader is done?โ€ - -## Choose where the content should live - -- Prefer updating an existing doc over creating a new one. -- Place content in the most discoverable location: - - `README.md`: project entry point and โ€œwhat is this?โ€. - - `docs/`: longer-form docs (follow existing conventions in that tree). - - Source KDoc: API usage, examples, and semantics that belong with the code. - -## Keep docs navigation in sync - -- When adding, removing, moving, or renaming a page under - `docs/content/docs/

/`, keep the current version's matching - `sidenav.yml` in sync. -- Use `docs/data/versions.yml` to identify the current documentation version for - that section. The current version is the entry with `is_main: true`; its - `version_id` maps to `docs/data/docs/
//sidenav.yml`. -- Do not update historical version entries or their navigation files unless the - user explicitly asks to edit that historical version. -- Map page files to `file_path` values relative to the current version's - `content_path`, without `.md`; `_index.md` maps to its directory path, such as - `01-getting-started/_index.md` -> `01-getting-started`. -- Keep each `page` label aligned with the page frontmatter `title` unless the - existing navigation intentionally uses a shorter reader-facing label. -- Preserve the existing ordering, nesting, keys, comments, and YAML quoting - style. Remove nav entries for deleted pages and update `file_path` values for - moved pages. -- If a docs content change should not appear in navigation, say so explicitly in - the final response. - -## Follow local documentation conventions - -- Follow `.agents/documentation-guidelines.md` and `.agents/documentation-tasks.md`. -- Use fenced code blocks for commands and examples; format file/dir names as code. -- When referencing a documentation page or section in body prose, use typographic - double quotation marks only if the visible reference text is the actual page or - section title, such as the โ€œGetting startedโ€ page or the โ€œTroubleshootingโ€ - section. The title normally starts with a capital letter. Do not add these - quotes around generic or descriptive links such as โ€œthis pageโ€, โ€œthe next - sectionโ€, โ€œdeclaring constraintsโ€, or `4.3`, even if they point to a page or - section. Do not add these quotes in โ€œWhatโ€™s nextโ€ sections or navigation - elements. Keep file paths, identifiers, frontmatter values, navigation labels, - and Markdown link labels in their expected syntax. -- In Markdown files, prefer footnote-style reference links for external `https://` - targets instead of inline links. Write readable body text like - `[label][short-id]`, then place the URL definition near the end of the file, - such as `[short-id]: https://example.com/long/path`. Keep reference IDs short - and descriptive. Inline links are still fine for local relative paths. -- Avoid widows, runts, orphans, and rivers by reflowing paragraphs when needed. - -## Make docs actionable - -- Prefer steps the reader can execute (commands + expected outcome). -- Prefer concrete examples over abstract descriptions. -- Include prerequisites (versions, OS, environment) when they are easy to miss. -- Use consistent terminology (match code identifiers and existing docs). - -## KDoc-specific guidance - -- For public/internal APIs, include at least one example snippet demonstrating common usage. -- When converting from Javadoc/inline comments to KDoc: - - Remove HTML like `

` and preserve meaning. - - Prefer short paragraphs and blank lines over HTML formatting. - -## Validate changes - -- For code changes, follow `.agents/running-builds.md`. -- For documentation-only changes in Kotlin/Java sources, prefer `./gradlew dokka`. diff --git a/.agents/skills/writer/agents/openai.yaml b/.agents/skills/writer/agents/openai.yaml deleted file mode 100644 index 44eaa4e241..0000000000 --- a/.agents/skills/writer/agents/openai.yaml +++ /dev/null @@ -1,5 +0,0 @@ -interface: - display_name: "Writer" - short_description: "Write and update user/developer docs" - default_prompt: "Write or revise documentation in this repository (for example: README.md, docs/**, CONTRIBUTING.md, and API documentation/KDoc). Follow local documentation guidelines in .agents/*.md, keep changes concise and actionable, and include concrete examples and commands where appropriate." - diff --git a/.agents/skills/writer/assets/templates/doc-page.md b/.agents/skills/writer/assets/templates/doc-page.md deleted file mode 100644 index f405b71e15..0000000000 --- a/.agents/skills/writer/assets/templates/doc-page.md +++ /dev/null @@ -1,23 +0,0 @@ -# Title - -## Goal - -State what the reader will accomplish. - -## Prerequisites - -- List versions/tools the reader needs. - -## Steps - -1. Do the first thing. -2. Do the next thing. - -## Verify - -Show how the reader can confirm success. - -## Troubleshooting - -- Common failure: likely cause โ†’ fix. - diff --git a/.agents/skills/writer/assets/templates/kdoc-example.md b/.agents/skills/writer/assets/templates/kdoc-example.md deleted file mode 100644 index fdbd9b6a0d..0000000000 --- a/.agents/skills/writer/assets/templates/kdoc-example.md +++ /dev/null @@ -1,11 +0,0 @@ -````kotlin -/** - * Explain what this API does in one sentence. - * - * ## Example - * ```kotlin - * // Show the typical usage pattern. - * val result = doThing() - * ``` - */ -```` diff --git a/.agents/skills/writer/assets/templates/kotlin-java-example.md b/.agents/skills/writer/assets/templates/kotlin-java-example.md deleted file mode 100644 index 5517516f56..0000000000 --- a/.agents/skills/writer/assets/templates/kotlin-java-example.md +++ /dev/null @@ -1,13 +0,0 @@ -{{< code-tabs langs="Kotlin, Java">}} - -{{< code-tab lang="Kotlin" >}} -```kotlin -``` -{{< /code-tab >}} - -{{< code-tab lang="Java" >}} -```java -``` -{{< /code-tab >}} - -{{< /code-tabs >}} diff --git a/.agents/tasks/archive/raise-coverage-kover-migration.md b/.agents/tasks/archive/raise-coverage-kover-migration.md new file mode 100644 index 0000000000..268ba07806 --- /dev/null +++ b/.agents/tasks/archive/raise-coverage-kover-migration.md @@ -0,0 +1,449 @@ +--- +slug: raise-coverage-kover-migration +branch: coverage-tests-skill +owner: claude +status: in-review +started: 2026-05-30 +--- + +## Goal + +Extend the `raise-coverage` skill with a precondition step that migrates a +consumer repo from the vanilla JaCoCo Gradle plugin to JetBrains Kover (with +`useJacoco(version = Jacoco.version)` so the engine and the JaCoCo-format XML +remain unchanged). The skill becomes **Kover-only** post-migration. Adjacent +JaCoCo-distribution code in `config`'s `buildSrc` is marked deprecated with a +pointer to the Kover path; no behaviour change for repos that still consume +the deprecated script plugins. + +## Context + +The skill in `.agents/skills/raise-coverage/` currently supports two coverage +frontends: Kover (consumer repos) and raw JaCoCo (the `config` repo itself). +Per user decision, the skill collapses to Kover-only and gains a Step 0 that +detects vanilla JaCoCo, proposes a one-shot repo-wide migration, waits for +approval, applies it, smoke-checks, and only then resumes the normal flow. + +`config` itself has no production Kotlin/Java code, so the skill never runs +*on* `config`. However, `config` distributes vanilla-JaCoCo infrastructure +(`buildSrc/src/main/kotlin/jacoco-kotlin-jvm.gradle.kts`, `jacoco-kmm-jvm.gradle.kts`, +and the `JacocoConfig` helper) that consumer repos may still apply โ€” these +get deprecation annotations + a runtime `logger.warn` (script plugins only) +but stay on disk so existing consumers keep building. `Jacoco.kt` (engine +version) and `Kover.kt` (plugin version) are unchanged because Kover uses +`useJacoco(version = Jacoco.version)`. + +### Decisions (locked โ€” do not re-litigate) + +| # | Decision | +|---|----------| +| 1 | Invocation: implicit precondition (Step 0 of the skill) | +| 2 | Scope: repo-wide, proposed once | +| 3 | Trigger: always, unless Kover is already applied everywhere | +| 4 | Both plugins applied: always remove `jacoco`, keep Kover | +| 5 | KMP: JVM-target-only migration via Spine's `kmp-module` script plugin โ€” uses the same `::koverXmlReport` task and `build/reports/kover/report.xml` path as Kotlin-JVM, because `kmp-module` configures only Kover's `total` report (no named variants, so no `koverXmlReport` task is generated) | +| 6 | CI / `.codecov.yml` / scripts: all updated to Kover paths and tasks | +| 7 | Plugin/engine version: reference `io.spine.dependency.test.Kover` / `Jacoco`; do not hardcode | +| 8 | Translation fidelity: best-effort full; flag unmappable constructs | +| 9 | Post-migration: skill flow is Kover-only | +| 10 | No-coverage case: silent install, no approval gate | +| 11 | Verification: smoke check only (`koverXmlReport` exists + parses) | +| 12 | `JacocoConfig` and `jacoco-*.gradle.kts`: mark deprecated, do not delete | + +### Verified facts (from Phase 1 exploration) + +- `buildSrc/src/main/kotlin/jvm-module.gradle.kts:54` applies Kover; `:99` + configures it with `useJacoco(version = Jacoco.version)` and XML-on-check. +- `buildSrc/src/main/kotlin/kmp-module.gradle.kts:74` and `:181` mirror the + above for KMP modules. +- `buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/` contains the + JaCoCo-pipeline classes to be deprecated: `JacocoConfig`, `TaskName`, + `CodebaseFilter`, `FileFilter`, `FileExtensions`, `FileExtension`, + `PathMarker`. +- `scripts/upload-artifacts.sh:38` references `build/jacoco*` directories. +- `.github/workflows/*.yml` in `config` contain no JaCoCo references. + +## Implementation + +Paths are relative to the `config` repo root. + +### Area A โ€” Skill files + +#### A1. `.agents/skills/raise-coverage/SKILL.md` + +- Frontmatter `description: >`: drop "Kover or JaCoCo frontend"; phrase the + report as "Kover's JaCoCo-format XML report"; add: "Before anything else, + ensures the repo is on Kover โ€” if vanilla JaCoCo is detected, proposes a + one-shot repo-wide migration and waits for approval." +- Body "with JaCoCo" โ†’ "with **Kover**'s JaCoCo-format XML report". +- **Scope** bullet rewritten Kover-only. Per-module task + `::koverXmlReport`; XML at + `/build/reports/kover/report.xml`. Same task and path on KMP + modules configured by Spine's `kmp-module` script plugin โ€” it sets up only + Kover's `total` report (no named variants, so no + `koverXmlReport` task is generated). Strip every "raw JaCoCo" / + `jacocoTestReport` / `jacocoRootReport` reference as a normal mode. +- **Insert new `## Step 0 โ€” Ensure Kover`** between `## Inputs` and + `## Workflow`. Three branches: + 1. Kover applied everywhere โ†’ silently proceed. + 2. Nothing applied anywhere โ†’ silently install Kover; record + "Migration: installed Kover" in the final Report; no approval gate. + 3. Vanilla JaCoCo in โ‰ฅ1 module โ†’ emit a proposal and **wait for approval**. +- **Proposal output structure** (Markdown, in this order): + 1. **Detected** โ€” every module applying `jacoco` / `JacocoPlugin` / + `JacocoConfig.applyTo` / a `jacoco-*.gradle.kts`; annotate "vanilla + only" vs. "JaCoCo+Kover both"; note any root `jacocoRootReport`. + 2. **Plan** โ€” file edits with paths (per-module `build.gradle.kts`, root + `build.gradle.kts`, `.codecov.yml`, `.github/workflows/*.yml`, + `scripts/*.sh`). + 3. **Translation notes** โ€” applicable rows from the migrate-to-kover table. + 4. **Manual-review surfaces** โ€” items the user must decide on. + 5. **Smoke check that will follow** โ€” the E1 commands. + 6. Close with: "Confirm to apply, or call out anything to change first." +- **Wait for approval.** No writes until explicit "go" / "yes" / "apply". + On adjustment requests, regenerate the proposal and wait again. +- **Apply** per `references/migrate-to-kover.md`; log `edited ` per + file. Any unresolved manual-review surface โ†’ stop ("needs your call on + ``"). +- **Smoke check** per E1. Failure โ†’ stop; do not fall through. +- **Resume** at Workflow step 1. +- **Workflow step 1** (`--triage`): "per-module `koverXmlReport`, or the + aggregate `jacocoRootReport` in `config`" โ†’ "per-module `koverXmlReport`, + or the root-level Kover aggregation task `koverXmlReport` if the repo + wires one". +- **Workflow step 2**: "Detect the coverage frontend and run โ€ฆ" โ†’ "Run + `::koverXmlReport`" โ€” same task on JVM and KMP modules configured + by Spine's `kmp-module` script plugin (no named variants โ†’ no + `koverXmlReport` task). Drop "either way" from the XML-parsing + sentence. +- **Workflow step 6**: "(`koverXmlReport` or `jacocoTestReport`)" โ†’ + `::koverXmlReport`. +- **Report**: add a **Migration** section (emitted only when Step 0 did work). +- **Safety**: add a bullet โ€” "No migration without explicit approval when + vanilla JaCoCo is detected. Silent install only when *no* frontend is in + place." + +#### A2. `.agents/skills/raise-coverage/references/coverage-signals.md` + +- Top blurb: drop the "two frontends" paragraph; replace with a single + paragraph stating that the engine is JaCoCo and the Spine convention is + Kover with `useJacoco(version = Jacoco.version)`; the XML is + JaCoCo-format either way. +- Delete the entire **"Two coverage frontends"** section (current lines + 11โ€“57). Replace with **"Where the report lives"** โ€” per-module task / XML + path, root-level aggregation paths, `find` recipe if unknown. +- **"Generating a report"**: drop the two JaCoCo `./gradlew` lines; keep + Kover. Same task on KMP modules configured by Spine's `kmp-module` script + plugin โ€” no `koverXmlReport` task is generated unless a named + `variant("โ€ฆ") { โ€ฆ }` block is declared. +- **"Extracting gaps for a class"**: drop "or the jacoco path". +- **"KMP / Kotlin-JVM modules"**: keep first sentence; delete the second + sentence about `jacoco-*-jvm` exec data paths. +- **Verification**: "the **same** report task" โ†’ `::koverXmlReport`. +- **Codecov triage tier appendix**: keep verbatim. + +#### A3. `.agents/skills/raise-coverage/references/migrate-to-kover.md` (new) + +Eight sections: + +1. **Purpose** โ€” one paragraph; link to + and the + `migrations/migration-to-0.7.0.html` migration guide. +2. **Detection signals** โ€” grep patterns per module's `build.gradle.kts`: + - Plugin block: `^\s*jacoco\b` inside `plugins {`, `id\("jacoco"\)`, + `apply\(\)`, `apply\(plugin = "jacoco"\)`. + - Script plugin: `apply\(plugin = "jacoco-`. Covers `jacoco-kotlin-jvm`, + `jacoco-kmm-jvm`. + - `JacocoConfig`: `JacocoConfig\.applyTo` or imports of + `io.spine.gradle.report.coverage.JacocoConfig`. + - DSL: `jacoco\s*\{`, `jacocoTestReport\s*\{`, + `jacocoTestCoverageVerification\s*\{`, `tasks\.named\("jacoco`. + - Kover applied: `org.jetbrains.kotlinx.kover`, or `id("jvm-module")` / + `id("kmp-module")` (both auto-apply Kover). + - Root aggregation: `jacocoRootReport`. + - Multi-module walk: parse `settings.gradle.kts` for `include(...)`; + honor `project(":x").projectDir = file(...)` overrides. +3. **Per-module migration**: + - Add Kover via `id(Kover.id)` if `buildSrc` is on the classpath; + otherwise `id("org.jetbrains.kotlinx.kover") version ""`. + If `jvm-module` / `kmp-module` is applied, skip the add (log "already + via jvm-module"). + - Strip `jacoco` from `plugins { }`. + - **Translation table**: + | JaCoCo construct | Kover / action | + |---|---| + | `jacoco { toolVersion = Jacoco.version }` | drop (engine version โ†’ root `useJacoco(...)`) | + | `jacoco { toolVersion = }` | **flag** | + | `reports { xml=true; html=true; csv=false }` | `kover { reports { total { xml { onCheck.set(true) }; html { } } } }` | + | `executionData.setFrom(...)` | **flag** (Kover-managed) | + | `sourceDirectories.setFrom(...)` | **flag** (Kover-inferred) | + | `classDirectories.setFrom(...)` (Kotlin-JVM/KMP `walkBottomUp`) | drop; **flag** if non-Kotlin | + | `reports.xml.outputLocation.set(...)` | **flag** (fixed path) | + | `tasks.named("jacocoTestReport") { dependsOn(...) }` | rewrite to `tasks.named("koverXmlReport")` | + | `violationRules { rule { limit { counter; value; minimum } } }` | `kover { reports { verify { rule { โ€ฆ } } } }`; counter map: INSTRUCTION/BRANCH/LINE = same; METHOD โ†’ INSTRUCTION + flag; CLASS โ†’ flag | + - `jvm-module` / `kmp-module` simplification: Kover already there; + migration becomes "remove JaCoCo bits only". +4. **Root-level aggregation**. Trigger: source had `jacocoRootReport` **or** + >1 module to aggregate. + - Apply Kover at root (skip if root applies `jvm-module`). + - `dependencies { kover(project(":foo")); โ€ฆ }` per consuming module. + - `kover { useJacoco(version = Jacoco.version); reports { total { xml { onCheck.set(true) }; html { } } } }`. + - Mirror per-module `violationRules` to root `verify { rule { โ€ฆ } }` + only if the source repo had a root-level rollup. +5. **CI / `.codecov.yml` / scripts** โ€” substitutions: + - Workflows: `jacocoTestReport` โ†’ `koverXmlReport`; `jacocoRootReport` โ†’ + root `koverXmlReport`; + `build/reports/jacoco/test/jacocoTestReport.xml` and + `build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml` โ†’ + `build/reports/kover/report.xml`. + - `.codecov.yml`: same path tokens; preserve `ignore` and + `coverage.status` verbatim. + - `scripts/*.sh`: `build/jacoco*` glob โ†’ `build/reports/kover`; **flag** + scripts reading raw `.exec` paths (e.g., + `scripts/upload-artifacts.sh:38` in `config`). +6. **KMP recipe**. JVM-only target. Same task and XML path as Kotlin-JVM: + `::koverXmlReport` and `/build/reports/kover/report.xml`. + Spine's `kmp-module` script plugin configures only Kover's `total` report, + so no `koverXmlReport` task is generated โ€” CI / `.codecov.yml` + must reference the unsuffixed path. A `koverXmlReportJvm` task would only + appear if a named `variant("jvm") { โ€ฆ }` block were declared, which + `kmp-module` does not do. +7. **Manual-review surfaces** (flag and ask): + - Custom `sourceDirectories` / `classDirectories` on `jacocoTestReport` + (the `jacoco-*-jvm.gradle.kts` pattern). + - Custom `reports.xml.destination` / `outputLocation`. + - Custom `executionData` paths. + - Indirect `jacoco.toolVersion` (property files, `gradle.properties`). + - Multi-pipeline setups where both reports are intentional. + - `JacocoConfig.applyTo(rootProject)` outside `config`. + - Custom convention plugins applying JaCoCo under a non-`jacoco-โ€ฆ` name. + - Non-JVM KMP targets โ€” out of scope (decision 5). +8. **References** โ€” links to the Kover migration guide and DSL docs. + +#### A4. `.agents/skills/raise-coverage/agents/openai.yaml` + +- `short_description`: "Migrate to Kover if needed, then generate unit tests + to close coverage gaps." +- `default_prompt`: rewrite to name Step 0 โ€” detect setup; if vanilla + JaCoCo found, propose a one-shot repo-wide migration and wait for + approval; if nothing applied, install Kover silently. After smoke check, + run the existing flow (localize from Kover XML; propose; approve; + generate Kotest / Truth stubs; re-verify). End with: "Tests-only changes + do not require a version bump." + +### Area B โ€” BuildSrc deprecations in `config` + +#### B1. `buildSrc/src/main/kotlin/jacoco-kotlin-jvm.gradle.kts` and `jacoco-kmm-jvm.gradle.kts` + +Runtime behaviour unchanged. Two edits per file: + +1. Below the copyright header, insert a `// DEPRECATED:` block: "This + script plugin distributes vanilla JaCoCo. New code should apply + `jvm-module` (or `kmp-module`), which configures Kover via + `useJacoco(version = Jacoco.version)`. The `raise-coverage` skill + migrates existing consumers. Kept so older consumer repos continue to + build; will be removed in a future release." +2. Immediately before `plugins { jacoco }`, add + `logger.warn("'jacoco-kotlin-jvm' is deprecated; use 'jvm-module' which applies Kover. See .agents/skills/raise-coverage/references/migrate-to-kover.md.")` + (use `jacoco-kmm-jvm` and `kmp-module` in the KMM file). + +#### B2. `@Deprecated` on JaCoCo-pipeline classes under `buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/` + +All `DeprecationLevel.WARNING`, no `ReplaceWith` (the replacement is a +multi-block DSL). + +- `JacocoConfig.kt` โ€” class `JacocoConfig`: "Use Kover's root-level + aggregation (`dependencies { kover(project(...)) }` plus + `kover { useJacoco(version = Jacoco.version); reports { total { xml { onCheck = true } } } }`) + instead of `JacocoConfig.applyTo(...)`. The `raise-coverage` skill + performs this migration automatically." +- `TaskName.kt` โ€” enum: "Internal task-name catalog for the deprecated + `JacocoConfig` pipeline; Kover uses its built-in task names + (`koverXmlReport`, etc.). Removed when `JacocoConfig` is." +- `CodebaseFilter.kt` โ€” class: "Used only by the deprecated `JacocoConfig`. + Kover infers source sets and respects `kover { filters { excludes { โ€ฆ } } }`." +- `FileFilter.kt` โ€” object: same wording as `CodebaseFilter`. +- `FileExtensions.kt` โ€” `@file:Deprecated("Path/extension helpers used only by the deprecated `JacocoConfig` pipeline. Removed when `JacocoConfig` is.")`. +- `PathMarker.kt` enum and `FileExtension.kt` enum โ€” same wording as + `FileExtensions`. + +For in-package self-references, accept the warnings or apply +`@Suppress("DEPRECATION")` on the call sites. + +#### B3. Untouched + +`buildSrc/src/main/kotlin/io/spine/dependency/test/Jacoco.kt` and `Kover.kt` +remain unchanged. `Jacoco.kt` is the engine-version source used by +`kover { useJacoco(version = Jacoco.version) }`. + +### Area C โ€” Adjacent docs in `config` + +#### C1. `.agents/tasks/raise-coverage.md` + +- Decisions table "Coverage engine" row: "exposed via Kover + (`koverXmlReport`); when a consumer repo still has vanilla JaCoCo, the + skill migrates it first; Codecov deferred." +- "Verified facts": replace the **JaCoCo paths** bullet with a Kover-paths + bullet (`/build/reports/kover/report.xml` โ€” same on KMP via + `kmp-module`, which configures only the `total` report; root aggregation + `build/reports/kover/report.xml`; Kover manages exec data internally). +- Plan: append `- [ ]` "Kover-only pivot: implement + `.agents/tasks/raise-coverage-kover-migration.md`". +- Log: append a 2026-05-30 entry summarising the pivot. +- Keep `status: draft`. + +#### C2. `.claude/commands/raise-coverage.md` + +- Description: "Ensure the repo is on Kover (migrate from JaCoCo if + needed), then localize coverage gaps and generate missing unit tests for + a module or path." +- Order bullet: `::jacocoTestReport` โ†’ `::koverXmlReport`. + `--triage` line: "ranked JaCoCo gap report" โ†’ "ranked Kover gap report". +- After the Skill bullet, add: "First-time setup: the skill enforces + Kover. If vanilla JaCoCo is found anywhere, the skill proposes a + repo-wide migration and **waits for your approval**. See + `references/migrate-to-kover.md`." +- `allowed-tools`: unchanged. + +#### C3. Other docs + +- `.agents/_TOC.md` โ€” no edit. +- `.agents/tasks/buildsrc-gradle-review-findings.md` โ€” above each item + referencing `jacoco-kmm-jvm.gradle.kts` or `JacocoConfig.kt` (lines + 77โ€“78, 101โ€“103, 121โ€“122, 169โ€“172, 201โ€“203), insert one line: + "**Superseded by Kover-only migration**: these files are deprecated; do + not invest in micro-rewrites. See + `.agents/skills/raise-coverage/references/migrate-to-kover.md`." +- `scripts/upload-artifacts.sh` โ€” add `# DEPRECATED:` comment line above + line 38 (`JACOCO_REPORTS=โ€ฆ`) pointing at the migration reference. No + behaviour change. +- `scripts/buildSrc-migration.kts`, `migrate`, `lychee.toml` โ€” no edits. + +### Area E โ€” Verification + +#### E1. Step 0 smoke check (post-migration) + +1. Run `./gradlew ::koverXmlReport --quiet` on the smallest leaf + JVM migrated module; if the root was touched, also `./gradlew + koverXmlReport --quiet`. +2. Assert `/build/reports/kover/report.xml` exists, is non-empty, + and the first non-XML-declaration line contains `Kt` synthetic) and emits both `FQN` and + `FQN$*` exclusion patterns to cover nested classes. The skill's + migration step now rewrites `JacocoConfig.applyTo` โ†’ `KoverConfig.applyTo` + instead of deleting the call. `./gradlew -p buildSrc compileKotlin` + green. diff --git a/.agents/tasks/archive/raise-coverage.md b/.agents/tasks/archive/raise-coverage.md new file mode 100644 index 0000000000..b0fc517ef1 --- /dev/null +++ b/.agents/tasks/archive/raise-coverage.md @@ -0,0 +1,283 @@ +--- +slug: raise-coverage +branch: coverage-tests-skill +owner: claude +status: draft +started: 2026-05-29 +updated: 2026-05-30 +--- + +## Goal + +Stand up a reusable, Spine-native agent skill โ€” **`raise-coverage`** โ€” that raises +JVM test coverage by localizing uncovered lines/branches with JaCoCo and +generating policy-compliant unit tests. The skill lives in `config` and +propagates to all ~50 repos via `./config/pull`. Success = the skill and its +wrappers are authored, distribution is wired, and the full loop has been +dry-run on one `base-libraries` module locally (nothing committed). + +## Context + +Scoped in Claude Chat; the produced `SKILL.md` was lost and the two surviving +files (`coverage-signals.md`, `coverage-tests.md`) are **drafts** to be +rewritten, not shipped. Clarification produced eight decisions that narrow and +simplify the original draft. + +### Decisions (locked โ€” do not re-litigate) + +| Decision | Choice | +|---|---| +| Skill name | **`raise-coverage`** (verb-noun, like `write-docs` / `bump-version`) | +| Workflow | localize โ†’ propose cases โ†’ **wait for approval** โ†’ generate โ†’ verify; plus read-only `--triage` | +| Coverage engine | **JaCoCo engine**, exposed via **Kover** (`koverXmlReport`); when a consumer repo still has vanilla JaCoCo, the skill migrates it first (see `raise-coverage-kover-migration.md`); Codecov deferred | +| Test language | **Kotlin + Kotest** for every new test, regardless of the language of the code under test; class names use the **`Spec`** suffix (e.g. `AbstractSourceFileSpec`). Truth proto extension is reachable only when Kotest cannot express the assertion | +| Scratch dir | reuse existing `tmp/` โ†’ `tmp/base-libraries` (already gitignored via `/tmp`) | +| Done bar | full loop on one `base-libraries` module, **local, nothing committed** | +| Codex parity | include `agents/openai.yaml` | +| Branch/task | keep branch `coverage-tests-skill`; `git mv` task file โ†’ `raise-coverage.md` | + +### Verified facts (baked into the deliverables) + +- **Skill system**: source of truth is `.agents/skills//`; `.claude/skills` + is a **symlink** to `../.agents/skills` (author once). `.claude/commands/.md` + is the slash-command wrapper. Action skills also ship `agents/openai.yaml`. +- **Distribution**: `migrate` (sourced by `pull`) copies the whole `.agents` + + `.claude` tree, so a new skill auto-propagates โ€” **except** a Hugo-only-repo + prune block that strips JVM-specific skills. `raise-coverage` is JVM-specific + and must be added to that block. +- **Test stack**: Kotest `6.1.11` (`io.kotest:kotest-assertions-core`), Google + Truth `1.4.4` (`truth` + `truth-proto-extension`), JUnit `6.0.3` + (`org.junit:junit-bom`), JaCoCo `0.8.14` (`Jacoco.kt`, already bumped in the + working tree). +- **House test idiom**: JUnit Jupiter structure (`@Test` / `@Nested` / + `@DisplayName` / `@TempDir`) + **Kotest matchers** (`shouldBe`, `shouldThrow`, + `shouldContainExactlyInAnyOrder`). NOT pure Kotest specs. (Verified in + `buildSrc/src/test/.../FileExtensionsTest.kt`.) +- **Kover paths** (post-migration): per-module + `/build/reports/kover/report.xml` โ€” same on Kotlin-JVM and KMP + modules configured by Spine's `kmp-module` script plugin, which sets up + only the `total` Kover report (no named variants, so no + `koverXmlReport` task is generated). Root aggregation (when wired): + `build/reports/kover/report.xml`. Kover manages exec data internally โ€” no + raw `.exec` paths are exposed to consumers. +- **Runtime successor to `JacocoConfig`**: + `io.spine.gradle.report.coverage.KoverConfig.applyTo(rootProject)`. Applies + the Kover plugin at the root, wires `dependencies { kover(project(...)) }` + for every Kover-enabled subproject, pins + `useJacoco(version = Jacoco.version)`, and pushes the union of generated-class + FQNs as Kover excludes into both per-module and root reports. Preserves the + generated-code-filtering behavior previously provided by `CodebaseFilter` so + the skill does not hallucinate gaps for generated classes. +- **Never test**: generated code (any path containing `generated`), `examples`, + existing `test` sources. `.codecov.yml` scope is `src/main/**` only. +- **No version bump** for tests-only changes (contrast with other action skills, + which end by invoking `/version-bumped`). + +## Deliverables (file-by-file) + +1. **`.agents/skills/raise-coverage/SKILL.md`** โ€” frontmatter `name` + + `description: >`. Sections: Goal/scope (**Kover-only**, using its + JaCoCo-format XML report; human `src/main` only) ยท Inputs (`$ARGUMENTS` + = `:module` | path | `--triage`) ยท Step 0 โ€” Ensure Kover (read-only + under `--triage`; silent install when no coverage plugin is in place; + **propose-and-wait** when vanilla JaCoCo is detected, per + `references/migrate-to-kover.md`) ยท Workflow (1 resolve target โ†’ + 2 localize gaps from Kover's `report.xml` โ†’ 3 read code-under-test + + existing tests + collaborators โ†’ 4 **propose test-case list and WAIT**; + `--triage` stops here with the ranked report โ†’ 5 generate โ†’ 6 verify) ยท + Test-generation rules (stubs not mocks; **Kotlin + Kotest** universal โ€” + JUnit Jupiter structure with Kotest assertions, written in Kotlin even + when the code under test is Java; class names use the **`Spec`** suffix; + `truth-proto-extension` is reachable only as an isolated fallback for + Protobuf assertions Kotest cannot express; cover edge cases; scaffold + `when`/sealed branches; skip generated/excluded paths) ยท Report format ยท + Safety (`--triage` is read-only; migration requires approval when + vanilla JaCoCo is detected; never weaken a `.codecov.yml` target; never + add a mocking dependency; read-only until step-4 approval; no version + bump for tests-only). + +2. **`.agents/skills/raise-coverage/references/coverage-signals.md`** โ€” JaCoCo + mechanics (rewritten from the draft, corrected to this repo): per-module vs + `jacocoRootReport`; the real report paths above; XML structure and gap rules + (`ci==0` uncovered line, `mb>0` partial branch); `xmllint`/Python extraction + recipes; the generated-code exclusion; `.codecov.yml` scope; KMP + source-set/exec-data variants. Ends with a short **"Future: Codecov triage + tier"** appendix capturing the deferred two-tier design. + +3. **`.agents/skills/raise-coverage/agents/openai.yaml`** โ€” Codex parity + (`interface.display_name` / `short_description` / `default_prompt`). + +4. **`.claude/commands/raise-coverage.md`** โ€” thin slash-command wrapper. + `allowed-tools: Read, Edit, Write, Grep, Glob, Bash(./gradlew:*), + Bash(git status:*), Bash(find:*)` (dropped `curl`/`WebFetch` โ€” Codecov + deferred). Body points at the skill, states the order, honors `testing.md` + + `coding-guidelines.md`, notes no version bump for tests-only. + +5. **`.agents/_TOC.md`** โ€” add `23. [Raise test coverage](skills/raise-coverage/SKILL.md)`. + +6. **`migrate`** โ€” add `raise-coverage` to the Hugo-only prune block: + `rm -rf ../.agents/skills/raise-coverage`, + `rm -rf ../.claude/skills/raise-coverage`, + `rm -f ../.claude/commands/raise-coverage.md` (mirrors existing JVM-skill entries). + +7. **`.agents/tasks/raise-coverage.md`** โ€” this file (`git mv` from + `improve-test-coverage.md`). + +> Dropped from the original plan: the standalone "install README". We author +> directly in `config`, so there is no separate install step โ€” placement is +> documented here. + +## Reusable test harness + +```bash +# from the config repo root +mkdir -p tmp +git clone --recurse-submodules https://github.com/SpineEventEngine/base-libraries tmp/base-libraries +cd tmp/base-libraries && git submodule update --init --recursive + +# lay down the published .agents/.claude baseline +./config/pull + +# overlay the in-development skill on top of the baseline +cp -R /.agents/skills/raise-coverage .agents/skills/ +cp /.claude/commands/raise-coverage.md .claude/commands/ +# (.claude/skills โ†’ ../.agents/skills symlink resolves the skill automatically) +``` + +`pull` fetches the **published** config from master, so it won't contain +`raise-coverage` yet โ€” the overlay-copy injects the in-dev version. Then execute +the skill's procedure against one module and verify. + +## Plan + +- [x] Housekeeping: `git mv` task file โ†’ `raise-coverage.md`; `TaskCreate` to track. +- [x] Author `SKILL.md`, `references/coverage-signals.md`, `agents/openai.yaml`, + and the `.claude/commands/raise-coverage.md` wrapper. +- [x] Wire-up: add `_TOC.md` entry; add `raise-coverage` to the `migrate` prune block. +- [x] Harness + pilot: cloned `base-libraries` into `tmp/`, ran `./config/pull`, + overlaid the skill. Localized gaps via Kover (`koverXmlReport`), then closed + `EnvironmentType.equals()`/`hashCode()` with a Java+Truth test โ€” the gap went + to zero (4 tests green, nothing committed). Hardened `SKILL.md` + + `coverage-signals.md` for the Kover frontend and for non-actionable + (inline / unreachable) gaps surfaced by the pilot. +- [x] Review: `review-docs` over the new Markdown; sanity-check the `migrate` + edit; confirm nothing staged in `tmp/base-libraries`. +- [x] Kover-only pivot: implemented `.agents/tasks/raise-coverage-kover-migration.md`. + Dropped dual-frontend logic, added Step 0 migration gate to `SKILL.md`, + and deprecated the JaCoCo-pipeline `buildSrc` helpers (`JacocoConfig`, + `CodebaseFilter`, `FileFilter`, `FileExtensions`, `FileExtension`, + `PathMarker`, `TaskName`, plus the `jacoco-*-jvm.gradle.kts` script + plugins). Authored `KoverConfig.kt` as the live runtime successor โ€” + preserves the generated-code exclusion previously provided by + `CodebaseFilter` and wires per-subproject `kover(project(...))` + aggregation, with the union of generated FQNs pushed into both + per-module and root reports. Three review/fix cycles + (`kotlin-review` + `review-docs`, parallel) all returned APPROVE; + `./gradlew -p buildSrc compileKotlin` green. +- [x] Re-run pilot with the updated skill against `tmp/base-libraries` + (`--triage` first, then close one fresh gap). Step 0 correctly detected + the hybrid state (root vanilla JaCoCo + subprojects already on Kover via + `module` script plugin), emitted the structured proposal, and on approval + migrated the root build (drop `jacoco` plugin; swap `JacocoConfig` โ†’ + `KoverConfig`; lift the call out of `gradle.projectsEvaluated`). Smoke + check passed: `:base:koverXmlReport` and root `koverXmlReport` both + produce JaCoCo-format XML with ``; generated + `OptionsProto` correctly excluded. Closed gaps in + `io.spine.code.fs.AbstractSourceFile` with a Java + Truth + `@TempDir` + stub (6 cases): coverage delta 20 missed LINE โ†’ 2, 2 missed BRANCH โ†’ 0 + (LINE 91 %, BRANCH 100 %). The residual 2 missed LINE are non-actionable + (`throw newIllegalStateException(...)` where the helper throws + internally) โ€” pattern added to `references/coverage-signals.md`. +- [ ] On merge: flip `status: done` and delete this task file per the + `.agents/tasks/` lifecycle. + +## Verification (the done bar) + +- Files: `_TOC.md` resolves; `.claude/skills/raise-coverage/SKILL.md` resolves + through the symlink; `openai.yaml` parses. +- Distribution: confirm the `migrate` prune block lists `raise-coverage` so + Hugo-only repos won't receive it. +- End-to-end โœ…: in `tmp/base-libraries`, `:environment:koverXmlReport` ran; + `EnvironmentTypeTest` (Java + Truth, 4 tests) compiles and passes; re-parsing + `build/reports/kover/report.xml` shows `EnvironmentType` `missedLINE`/ + `missedBRANCH` โ†’ 0 (was 2 / 1). **Nothing committed to base-libraries.** + +## Risks / notes + +- **Build feasibility**: `base-libraries` must build here (JDK + network; first + build slow). Mitigation: build only the pilot module's test+report tasks. If it + can't build, fall back to the analysis dry-run for the pilot and flag it โ€” the + four authored files still ship. +- **Pre-existing working-tree changes**: `Jacoco.kt` (โ†’`0.8.14`) and the old task + file are already modified. Leave `Jacoco.kt` alone (align docs to 0.8.14); only + `git mv`/rewrite the task file. +- **No commits/pushes** anywhere unless explicitly authorized. + +## Log + +- 2026-05-29 โ€” Shortlisted candidate skills; selected `clear-solutions` as the + structural base with Kotest/taxonomy donors. +- 2026-05-29 โ€” Recorded original decisions (mixed Java+Kotlin, Codecov + JaCoCo, + author in `config` for all repos); drafted SKILL/reference/command/install. +- 2026-05-30 โ€” Re-scoped with the user. Renamed skill `coverage-tests` โ†’ + **`raise-coverage`**; **deferred Codecov** (JaCoCo-only v1); confirmed + Kotlinโ†’Kotest / Javaโ†’Truth; chose `tmp/` scratch dir; set the done bar to a + local full-loop on a `base-libraries` module; added `openai.yaml` + `migrate` + prune-block deliverables. Verified the test stack and JaCoCo report paths from + `buildSrc`. Rewrote this task file from the plan; awaiting review. +- 2026-05-30 โ€” Pivot to Kover-only. Decided to collapse the skill to a single + frontend and add a Step 0 migration gate. When the skill detects vanilla + JaCoCo, it proposes a one-shot repo-wide migration and waits for approval; + when nothing is in place, it installs Kover silently. The vanilla-JaCoCo + helpers under `buildSrc` (`jacoco-kotlin-jvm` / `jacoco-kmm-jvm` script + plugins, `JacocoConfig` aggregator and its support classes) are deprecated, + not deleted, so existing consumers keep building. Full implementation plan + moved to `.agents/tasks/raise-coverage-kover-migration.md`. +- 2026-05-30 โ€” Built the four files + wire-up; ran the `base-libraries` pilot. + Findings that reshaped the skill: (1) consumer repos expose coverage through + **Kover** (`koverXmlReport`, JaCoCo engine, JaCoCo-format XML) โ€” not the + per-module `jacocoTestReport` the draft assumed โ€” so the skill is now + frontend-aware (Kover or raw JaCoCo). (2) `inline`/`reified` functions and + unreachable guards read as uncovered but are **non-actionable** (a passing test + for `parse` left `Parse.kt` `ci=0`), so the skill now filters them out. + Demonstrated true closure on `EnvironmentType.equals()`/`hashCode()` + (Java + Truth). `review-docs` running. +- 2026-05-30 โ€” Tightened the test-generation policy in `SKILL.md` and the + Codex `default_prompt`. New tests are always written in **Kotlin** (JUnit + Jupiter + Kotest assertions), regardless of whether the code under test is + Kotlin or Java, and test class names use the **`Spec`** suffix + (`AbstractSourceFileSpec`, not `AbstractSourceFileTest`). This matches the + `*Spec.kt` convention already in use across `base-libraries` and removes + the dual-language Truth-for-Java branch that the prior pilot followed by + accident. Truth (`truth-proto-extension`) stays reachable only for + Protobuf assertions Kotest cannot express. +- 2026-05-30 โ€” Re-ran the pilot end-to-end on `tmp/base-libraries`. Step 0 + detected vanilla JaCoCo at the root + Kover already applied in subprojects, + proposed the repo-wide migration, and on approval applied it. One lifecycle + gotcha surfaced: `KoverConfig.applyTo(root)` cannot live inside + `gradle.projectsEvaluated { โ€ฆ }` (Kover registers `afterEvaluate` hooks at + apply time). Documented in `references/migrate-to-kover.md` ยง3 and in the + `KoverConfig` class KDoc. Closed `AbstractSourceFile` with a Java + Truth + stub (6 cases via `@TempDir`); coverage went 20/2 missed LINE/BRANCH โ†’ 2/0. + Residual 2 LINE remained `mi=10 ci=0` despite passing tests โ€” root cause is + the Spine `Exceptions.newIllegalStateException` idiom (declared to return + the exception but throws internally), making `throw helper(...)` lines + unreachable for JaCoCo's downstream probe. Added the pattern to + `references/coverage-signals.md` as a third non-actionable category. +- 2026-05-30 โ€” Kover-only pivot landed. Implemented per + `.agents/tasks/raise-coverage-kover-migration.md`: collapsed the skill to a + single Kover frontend with a Step 0 migration gate that proposes a repo-wide + JaCoCo โ†’ Kover migration (waits for approval) when vanilla JaCoCo is + detected. Deprecated the `JacocoConfig` aggregator and its supporting helpers + (`CodebaseFilter`, `FileFilter`, `FileExtensions`, `FileExtension`, + `PathMarker`, `TaskName`) plus the `jacoco-*-jvm.gradle.kts` script plugins + โ€” kept on disk so existing consumers keep building. Authored `KoverConfig.kt` + as the live runtime successor (preserves generated-code exclusion via Kover + `filters { excludes { classes(...) } }` derived from source dirs containing + `generated/`; wires `kover(project(...))` aggregation for every Kover-enabled + subproject; pins the JaCoCo engine version). Doc set updated: + `references/migrate-to-kover.md` is the new mechanical recipe; + `SKILL.md` got the Step 0 proposal protocol; `coverage-signals.md` rewritten + Kover-only. Reviewed across three cycles (`kotlin-review` + `review-docs` + in parallel) โ€” all APPROVE, zero outstanding comments; + `./gradlew -p buildSrc compileKotlin` green. Nothing committed. diff --git a/.agents/tasks/raise-base-coverage.md b/.agents/tasks/raise-base-coverage.md new file mode 100644 index 0000000000..cfc6176fa0 --- /dev/null +++ b/.agents/tasks/raise-base-coverage.md @@ -0,0 +1,208 @@ +--- +slug: raise-base-coverage +branch: increse-coverage +owner: codex +status: in-progress +started: 2026-06-01 +related-memories: [] +--- + +## Goal + +Raise test coverage for the `:base` module while cleaning up stale public API. +Success means deprecated `:base` API is removed, `io.spine.code.fs` usage is +checked across SpineEventEngine projects, unused `io.spine.code.fs` types are +deprecated, the remaining non-deprecated API is covered with Kotlin tests using +Kotest assertions and no mocks, and the follow-up Kover report confirms targeted +gaps closed without weakening Codecov settings. + +## Context + +- Target module: `:base`. +- Coverage source: `base/build/reports/kover/report.xml`. +- Kover is applied via the shared `module` plugin and root `KoverConfig`. +- The original coverage-only plan would not require a version bump, but the + updated scope includes production API removal/deprecation and therefore must + be treated as a production-code change. +- The skill requires an approval pause after proposing test cases. +- `io.spine.code.fs` test coverage depends on the organization-wide usage + analysis: only API that remains non-deprecated should be covered. + +## Plan + +- [x] Load repo orientation, memory, testing rules, and coverage references. +- [x] Confirm Kover is already available for the target module. +- [x] Generate and parse the `:base` Kover XML report. +- [x] Read target sources and existing tests for selected gaps. +- [x] Document proposed concrete test cases for approval. +- [x] Remove deprecated API in the `:base` module. +- [x] Analyze whether `io.spine.code.fs` is used in SpineEventEngine projects. +- [x] Mark `io.spine.code.fs` types that are not used by any project as deprecated. +- [x] Deprecate `io.spine.code.fs` types that are not used. +- [x] Analyse whether `RejectionType` is used in Spine SDK projects and deprecate if not. +- [x] Finalize test cases for the remaining non-deprecated API and wait for + approval to write tests. +- [x] Add approved Kotlin `*Spec` tests using stubs, not mocks. +- [x] Re-run `:base:koverXmlReport` and confirm targeted gaps closed. + +## Updated Scope + +The plan now includes API cleanup before test generation: + +- Remove deprecated API from `:base`. The first known target from the selected + gaps is `FsObject.directory()`, which is deprecated in favor of `parent()`. + The implementation pass must scan the whole `:base` module for other + deprecated public API before editing. +- Analyze whether `io.spine.code.fs` is used by SpineEventEngine projects. + Check the current repository first, then sibling/local checkouts and GitHub + organization usage if local evidence is incomplete. +- Deprecate `io.spine.code.fs` types that have no organization usage. Do not + add coverage for API newly marked as deprecated. +- Cover only the `io.spine.code.fs` API that remains non-deprecated after the + usage analysis, plus the selected non-deprecated `File`/`Path` Unix + conversion extension branches. + +## Findings + +Step 0 found no migration work. Kover is already available through the shared +`module` plugin and root `KoverConfig`; no Gradle module inspected applies +vanilla JaCoCo. + +Generated report: + +- Command: `./gradlew :base:koverXmlReport --quiet`. +- XML: `base/build/reports/kover/report.xml`. +- Report format: JaCoCo XML emitted by Kover, with `report.dtd` DOCTYPE. +- Module totals: 3265/4212 lines covered (77.52%) and 750/1087 branches + covered (69.00%). + +Selected actionable gaps before the updated API cleanup: + +- `base/src/main/java/io/spine/code/fs/FsObject.java` โ€” lines `53`, `64`, + `71`, `77`, `82`, `87`, `92-99`; branches in `equals()`. Line `64` + belongs to the deprecated `directory()` method and should be removed instead + of covered. +- `base/src/main/java/io/spine/code/fs/AbstractSourceFile.java` โ€” lines + `63-71`, `78-84`, `92-100`; branches in `lines()`. +- `base/src/main/java/io/spine/code/fs/SourceCodeDirectory.java` โ€” lines + `44-56`. +- `base/src/main/java/io/spine/code/fs/AbstractDirectory.java` โ€” constructor + lines `40-41`, covered by instantiating a concrete test subclass. +- `base/src/main/kotlin/io/spine/io/Files.kt` โ€” lines `57-61`, `70-73`; + branches in `File.toUnix()` and `File.toUnixPath()`. The deprecated + `File.toUnix()` API was removed, so only `File.toUnixPath()` remains for + future coverage. +- `base/src/main/kotlin/io/spine/io/Paths.kt` โ€” lines `66-70`; branches in + `Path.toUnix()`. + +Non-actionable note: + +- `AbstractSourceFile.java` lines `69` and `82` are `throw helper(...)` lines + where the helper throws internally. They may remain as JaCoCo gaps even when + the surrounding `IOException` paths are exercised; this is documented in the + `raise-coverage` coverage-signals reference. + +## Deprecated API Removal + +Removed from production sources: + +- `io.spine.util.MoreCollections` deprecated aliases for + `Iterable.theOnly()` and `Iterable.interlaced(...)`. +- `Indent.DEFAULT_SIZE`; `DEFAULT_JAVA_INDENT_SIZE` remains. +- `File.toUnix()`; `File.toUnixPath()` remains. +- `Any.unpackGuessingType()`; `Any.unpackKnownType()` remains. +- `Identifier.findField(...)`; `Field.findIdField(...)` remains. +- `Durations2.ZERO`, `Durations2.isPositive(...)`, and + `Durations2.isNegative(...)`; Protobuf `Durations` replacements remain. +- `FsObject.directory()`; `FsObject.parent()` remains. +- `TypeUrl.toTypeName()`; `TypeUrl.typeName()` remains. +- `SourceFile.isRejections()`. +- Deprecated `CollectionsConverter` and its dedicated tests. +- Deprecated `Text` and its dedicated tests. + +For `Columns`, the mutator overrides are required by `List`, so they were not +removed. Their local deprecation markers and `@deprecated` Javadocs were +removed; the methods still throw `UnsupportedOperationException` and remain +annotated with `@DoNotCall`. + +Verification after removal: + +- `rg -n "@Deprecated|Deprecated\\(|@deprecated" base/src/main base/src/test` + returned no matches. +- `./gradlew :base:build --quiet` passed. + +## Proposed Cases + +These cases are provisional until the `io.spine.code.fs` usage analysis is +complete. Cases for API that becomes deprecated must be dropped; cases for API +that remains supported should be implemented. + +Add `base/src/test/kotlin/io/spine/code/fs/FsObjectSpec.kt`: + +- `FsObject` exposes path, parent, existence, and `toString()`. + Input: a temp file and a missing sibling path. + Expected: path and parent match, the real file exists, the missing file does + not, and `toString()` returns the path string. + Closes: `FsObject.java` lines `53`, `71`, `77`, and `82`. + +- `FsObject` equality and hash code are path-based. + Input: two simple concrete subclasses with the same path, and another + equality group with a different path. + Expected: `EqualsTester().addEqualityGroup(...).testEquals()`. + Closes: `FsObject.java` lines `87`, `92-99`, and the `equals()` branches. + +- `SourceCodeDirectory` resolves child directories and source files. + Input: tiny hand-written subclasses of `SourceCodeDirectory` and + `AbstractSourceFile`. + Expected: `root.resolve(child)` and `root.resolve(file)` return the resolved + `Path`. + Closes: `AbstractDirectory.java` constructor lines and + `SourceCodeDirectory.java` lines `44-56`. + +- `AbstractSourceFile` loads, exposes, updates, and stores lines. + Input: a temp text file with two lines. + Expected: `lines()` is empty before `load()`, loaded lines match file + content, `update(...)` replaces the in-memory lines, and `store()` rewrites + the file. + Closes: `AbstractSourceFile.java` lines `63-68`, `78-80`, `92-100`, and both + `lines()` branches. + +- `AbstractSourceFile.load()` rejects a missing file. + Input: non-existent temp path. + Expected: `shouldThrow`. + Closes: the precondition path at `AbstractSourceFile.java:64`. + +Extend `base/src/test/kotlin/io/spine/io/FilesSpec.kt`: + +- `File.toUnixPath()` converts Windows-style separators and returns the + original path string otherwise. + Closes: `Files.kt` lines `70-73`. + +Extend `base/src/test/kotlin/io/spine/io/PathsSpec.kt`: + +- `Path.toUnix()` converts Windows-style separators and returns the same + instance for Unix-style paths. + Closes: `Paths.kt` lines `66-70`. + +All proposed tests are Kotlin `*Spec` tests, use Kotest assertions, and rely on +small hand-written subclasses rather than mocks. Do not test removed or newly +deprecated API. + +## Log + +- 2026-06-01 12:29 WEST โ€” created task file; Kover is applied through + `module` / `KoverConfig`, with no vanilla JaCoCo applied by the Gradle + modules under inspection. +- 2026-06-01 12:30 WEST โ€” generated `:base` Kover XML report. Module totals: + 3265/4212 lines (77.52%) and 750/1087 branches (69.00%). Selected focused, + actionable gaps in `io.spine.code.fs` plus `File`/`Path` Unix conversion + extension branches for the approval proposal. +- 2026-06-01 12:32 WEST โ€” recorded the findings and proposed test cases in + this plan document. Awaiting approval before writing tests. +- 2026-06-01 12:34 WEST โ€” updated scope per user request: remove deprecated + `:base` API, analyze `io.spine.code.fs` usage across SpineEventEngine + projects, deprecate unused types, and cover only the non-deprecated API. +- 2026-06-01 12:58 WEST โ€” removed deprecated `:base` APIs, deleted dedicated + tests for removed deprecated types, refreshed copyright headers on modified + source files, confirmed no deprecated markers remain under `base/src`, and + passed `./gradlew :base:build --quiet`. diff --git a/.agents/testing.md b/.agents/testing.md deleted file mode 100644 index f81bdbf3d3..0000000000 --- a/.agents/testing.md +++ /dev/null @@ -1,8 +0,0 @@ -# ๐Ÿงช Testing - -- Do not use mocks, use stubs. -- Prefer [Kotest assertions][kotest-assertions] over assertions from JUnit or Google Truth. -- Generate unit tests for APIs (handles edge cases/scenarios). -- Supply scaffolds for typical Kotlin patterns (`when`, sealed classes). - -[kotest-assertions]: https://kotest.io/docs/assertions/assertions.html diff --git a/.agents/version-policy.md b/.agents/version-policy.md deleted file mode 100644 index 3e8abd5495..0000000000 --- a/.agents/version-policy.md +++ /dev/null @@ -1,19 +0,0 @@ -# Version policy - -When a repository has `version.gradle.kts` at the project root, it follows the -[Spine SDK Versioning policy][wiki-versioning]. The version is kept in that -file and follows [Semantic Versioning 2.0.0][semver] with Spine-specific -extensions (snapshot `NUMBER`, patch, and flavor suffixes). - -For repositories with root `version.gradle.kts`, PRs without a version bump -fail CI. Repositories without that file are not versioned Gradle Build Tools -projects; their version check is not applicable, and agents must not create -`version.gradle.kts` just to satisfy `/pre-pr`. - -For the bump procedure in repositories that have root `version.gradle.kts` โ€” -version-number selection, the commit-message convention, the rebuild, -dependency-report updates, and conflict resolution โ€” use the -[`bump-version`](skills/bump-version/SKILL.md) skill. - -[semver]: https://semver.org/ -[wiki-versioning]: https://github.com/SpineEventEngine/documentation/wiki/Versioning diff --git a/.agents/widow-runt-orphan.jpg b/.agents/widow-runt-orphan.jpg deleted file mode 100644 index 284b02a47d..0000000000 Binary files a/.agents/widow-runt-orphan.jpg and /dev/null differ diff --git a/.claude/agents b/.claude/agents new file mode 120000 index 0000000000..18e96c9560 --- /dev/null +++ b/.claude/agents @@ -0,0 +1 @@ +../.agents/shared/claude/agents \ No newline at end of file diff --git a/.claude/commands b/.claude/commands new file mode 120000 index 0000000000..ad85cd809c --- /dev/null +++ b/.claude/commands @@ -0,0 +1 @@ +../.agents/shared/claude/commands \ No newline at end of file diff --git a/.claude/commands/bump-gradle.md b/.claude/commands/bump-gradle.md deleted file mode 100644 index f9078802c9..0000000000 --- a/.claude/commands/bump-gradle.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -description: Upgrade the Gradle wrapper to the latest release. -argument-hint: "[gradle-version]" -allowed-tools: Read, Edit, Bash(./gradlew:*), Bash(git status:*), Bash(git diff:*), WebFetch ---- - -Follow the `bump-gradle` skill exactly: - -- Skill: `.agents/skills/bump-gradle/SKILL.md` -- Read the skill first. -- Use https://docs.gradle.org/current/release-notes.html as the source of truth for the latest version. Do NOT rely on remembered Gradle versions. -- If the user supplied a version: $ARGUMENTS, use it; otherwise read it from the release notes. -- Commit the wrapper change and dependency report change in separate commits per the skill. diff --git a/.claude/commands/bump-version.md b/.claude/commands/bump-version.md deleted file mode 100644 index 82e18b599c..0000000000 --- a/.claude/commands/bump-version.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -description: Bump the project version in version.gradle.kts per Spine SDK versioning policy. -argument-hint: "[snapshot|minor|major]" -allowed-tools: Read, Edit, Bash(git status:*), Bash(git diff:*), Bash(git log:*), Bash(./gradlew:*) ---- - -Follow the `bump-version` skill exactly: - -- Skill: `.agents/skills/bump-version/SKILL.md` -- Read the skill first; it owns the policy (snapshot numbering, version conflicts, rebuilding reports). -- Increment requested by the user: $ARGUMENTS (treat as "snapshot" if empty). -- Inspect `git status --short` before editing; preserve unrelated user changes. -- Stop and ask the user before committing. diff --git a/.claude/commands/dependency-update.md b/.claude/commands/dependency-update.md deleted file mode 100644 index 9c54da1498..0000000000 --- a/.claude/commands/dependency-update.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -description: Refresh external dependency versions in buildSrc to their latest non-snapshot release. -argument-hint: "[--dry-run] [paths...]" -allowed-tools: Read, Edit, Write, Grep, Glob, WebFetch, Bash(git status:*), Bash(git diff:*), Bash(./gradlew build:*), Bash(./gradlew clean build:*) ---- - -Follow the `dependency-update` skill exactly: - -- Skill: `.agents/skills/dependency-update/SKILL.md` -- Scope / flags: $ARGUMENTS -- Walk every dependency object under `buildSrc/src/main/kotlin/io/spine/dependency/`. -- Source of truth per artifact: the URL in the file's comment (line `// https://...` or KDoc `@see`). If no URL, fall back to Maven Central AND back-fill the URL comment. -- Filter out snapshots, RCs, alphas, betas, milestones, EAPs, and `-dev` builds. -- Apply the edit. Do NOT commit; emit the report described in the skill. -- Flag `local/` (Spine SDK) updates separately so the user can decide whether to bump in lockstep. -- After the run, suggest the user review the diff and run `./gradlew build` (or `./gradlew clean build` if proto files participate). For `local/` bumps, suggest `./gradlew buildDependants`. diff --git a/.claude/commands/java-to-kotlin.md b/.claude/commands/java-to-kotlin.md deleted file mode 100644 index 6f2c072f93..0000000000 --- a/.claude/commands/java-to-kotlin.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -description: Convert Java files to idiomatic Kotlin, including Javadoc -> KDoc. -argument-hint: "" -allowed-tools: Read, Edit, Write, Bash(./gradlew:*), Bash(git status:*), Grep, Glob ---- - -Follow the `java-to-kotlin` skill exactly: - -- Skill: `.agents/skills/java-to-kotlin/SKILL.md` -- Target: $ARGUMENTS -- Preserve behavior. Convert Javadoc to KDoc, `@Nullable` to nullable Kotlin types, getters/setters to properties, static methods to companion objects or top-level functions. -- After each file, run `./gradlew compileKotlin` (or the relevant module's compile task) to verify. -- Honor `.agents/coding-guidelines.md` for Kotlin idioms. diff --git a/.claude/commands/move-files.md b/.claude/commands/move-files.md deleted file mode 100644 index 25885f9d77..0000000000 --- a/.claude/commands/move-files.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -description: Move or rename files/directories, updating all references and build metadata. -argument-hint: " " -allowed-tools: Read, Edit, Bash(git mv:*), Bash(git status:*), Bash(git ls-files:*), Grep, Glob ---- - -Follow the `move-files` skill exactly: - -- Skill: `.agents/skills/move-files/SKILL.md` -- Operation: $ARGUMENTS -- Preflight (run `git status --short`, classify scope) -> Search for all old identifiers -> Move with `git mv` -> Repair references (imports, build metadata, docs) -> Verify. -- Report: Moved[], UpdatedRefs[], Verification[], Risks[]. diff --git a/.claude/commands/pre-pr.md b/.claude/commands/pre-pr.md deleted file mode 100644 index 24499cc517..0000000000 --- a/.claude/commands/pre-pr.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -description: Run the applicable pre-PR checklist (version gate, build/check, reviewers) and write a sentinel so `gh pr create` is unblocked. -argument-hint: "[base-ref]" -allowed-tools: Read, Write, Grep, Glob, Agent, Bash ---- - -Follow the `pre-pr` skill exactly: - -- Skill: `.agents/skills/pre-pr/SKILL.md` -- Base ref: $ARGUMENTS (treat empty as `master`). -- Detect whether the repository-root `version.gradle.kts` exists. If it is - absent at both the base ref and `HEAD`, the version check is `N/A`; do not - create the file and do not ask for `/bump-version`. -- Run the build/check command selected by the skill and - `.agents/running-builds.md`. The command may be Gradle or non-Gradle. -- Dispatch the reviewers as Claude subagents in parallel โ€” send a single - message with multiple Agent tool uses: - - `kotlin-review` when `.kt|.kts|.java` files changed. - - `review-docs` when `.md` files or KDoc inside sources changed. - - `dependency-audit` when any file under - `buildSrc/src/main/kotlin/io/spine/dependency/` changed. -- Pass the version-check status to reviewers. If it is `N/A`, tell them: - "This repository has no root `version.gradle.kts`; a version bump is not - applicable and must not be reported as missing." -- Each reviewer is read-only; do not pass it edit tools. -- On any reviewer returning `REQUEST CHANGES`, treat the overall result - as `FAIL` and stop before writing the sentinel as `PASS`. -- Sentinel location: `$(git rev-parse --show-toplevel)/.git/pre-pr.ok`, - format per the skill (`head=`, `branch=`, `status=`, `timestamp=`, - `build=`, `reviewers=`, `version=`). Use `git rev-parse HEAD` for the - SHA and `date -u +%Y-%m-%dT%H:%M:%SZ` for the timestamp. -- Do NOT run `gh pr create`. That is the user's next step. diff --git a/.claude/commands/review-docs.md b/.claude/commands/review-docs.md deleted file mode 100644 index f8043f0ea1..0000000000 --- a/.claude/commands/review-docs.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -description: Review documentation changes (KDoc/Javadoc and Markdown) against Spine documentation conventions. -argument-hint: "[base-ref | --staged | paths...]" -allowed-tools: Read, Grep, Glob, Bash(git diff:*), Bash(git log:*), Bash(git status:*), Bash(git rev-parse:*), Bash(git ls-files:*) ---- - -Follow the `review-docs` skill exactly: - -- Skill: `.agents/skills/review-docs/SKILL.md` -- Scope / flags: $ARGUMENTS - - Empty: review the current branch's diff against `master` (`git diff master...HEAD`). - - `--staged`: review staged changes only (`git diff --staged`). - - A base ref (e.g. `master`, `origin/master`, a commit SHA): review `git diff ...HEAD`. - - Explicit paths: limit the review to those paths in addition to the diff scope. -- The skill owns the procedure, the per-area checks (KDoc/Javadoc, Markdown, - prose flow, terminology), and the output format (Must fix / Should fix / - Nits + one-line verdict). -- Stay in scope: documentation only. If a code-quality issue surfaces, - note it briefly as a Nit pointing at `/review` (or the `kotlin-review` - agent) โ€” do not expand the review. -- Read-only: do not edit files, do not run builds. diff --git a/.claude/commands/run-build.md b/.claude/commands/run-build.md deleted file mode 100644 index 8a8d84ca0b..0000000000 --- a/.claude/commands/run-build.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -description: Build the project the right way based on what changed (proto vs. Kotlin/Java vs. docs). -allowed-tools: Bash(./gradlew:*), Bash(git status:*), Bash(git diff:*) ---- - -Decide which build to run by looking at `git status --short` and `git diff --name-only`: - -- If any `.proto` files changed: `./gradlew clean build` -- Else if Kotlin or Java source changed: `./gradlew build` -- Else if only docs/comments changed (KDoc / Javadoc / Markdown): `./gradlew dokka`. Tests are NOT required for doc-only changes. - -Report the chosen command and its result. See `.agents/running-builds.md`. diff --git a/.claude/commands/update-copyright.md b/.claude/commands/update-copyright.md deleted file mode 100644 index 076fb6133a..0000000000 --- a/.claude/commands/update-copyright.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -description: Refresh copyright headers from the IntelliJ profile, replacing today.year with the current year. -argument-hint: "[paths...]" -allowed-tools: Bash(python3 .agents/skills/update-copyright/scripts/update_copyright.py:*), Read ---- - -Follow the `update-copyright` skill exactly: - -- Skill: `.agents/skills/update-copyright/SKILL.md` -- Run: `python3 .agents/skills/update-copyright/scripts/update_copyright.py $ARGUMENTS` -- If $ARGUMENTS is empty, run once with `--dry-run`, show the output to the user, then run without `--dry-run`. -- Never add a header to a file that doesn't already have one. diff --git a/.claude/commands/write-docs.md b/.claude/commands/write-docs.md deleted file mode 100644 index b9b9a742b2..0000000000 --- a/.claude/commands/write-docs.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -description: Write or update Markdown / KDoc documentation per Spine documentation conventions. -argument-hint: "" -allowed-tools: Read, Edit, Write, Grep, Glob ---- - -Follow the `writer` skill exactly: - -- Skill: `.agents/skills/writer/SKILL.md` -- Topic / target: $ARGUMENTS -- Decide audience first (end user, contributor, maintainer, tooling). -- Prefer updating an existing doc over creating a new one. -- Keep `docs/data/docs/

//sidenav.yml` in sync when adding, removing, moving, or renaming pages under `docs/content/docs/
/`. -- Honor `.agents/documentation-guidelines.md` and `.agents/documentation-tasks.md`. diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000000..602e6ceaaa --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Skill(pre-pr)", + "Skill(pre-pr:*)", + "Bash(.agents/skills/version-bumped/scripts/version-bumped.sh)", + "Bash(echo \"exit=$?\")" + ] + } +} diff --git a/.claude/agents/dependency-audit.md b/.codex/agents/dependency-audit.toml similarity index 60% rename from .claude/agents/dependency-audit.md rename to .codex/agents/dependency-audit.toml index 9db010fe55..09518dd56c 100644 --- a/.claude/agents/dependency-audit.md +++ b/.codex/agents/dependency-audit.toml @@ -1,10 +1,6 @@ ---- -name: dependency-audit -description: Audits changes to dependency declarations under `buildSrc/src/main/kotlin/io/spine/dependency/` โ€” catches accidental version downgrades, BOM mismatches, missing deprecation markers, copyright drift, and convention drift. Use proactively whenever a diff touches that directory, or when the user asks "audit this dependency bump". Read-only; does not run builds. -tools: Read, Grep, Glob, Bash -model: claude-haiku-4-5-20251001 ---- - +name = "dependency-audit" +description = 'Audits changes to dependency declarations under `buildSrc/src/main/kotlin/io/spine/dependency/` โ€” catches accidental version downgrades, BOM mismatches, missing deprecation markers, copyright drift, and convention drift. Use proactively whenever a diff touches that directory, or when the user asks "audit this dependency bump". Read-only; does not run builds.' +developer_instructions = """ Follow the `dependency-audit` skill exactly: - Skill: `.agents/skills/dependency-audit/SKILL.md` @@ -16,4 +12,4 @@ Follow the `dependency-audit` skill exactly: - **Be fast.** Fetch the full unified diff once, work from it, and `Read` individual files only when the skill's step 2 budget allows. Issue independent `Grep`/`Bash` calls in parallel within a single response; - do not halt at the first failure โ€” collect all findings and report once. + do not halt at the first failure โ€” collect all findings and report once.""" diff --git a/.claude/agents/kotlin-review.md b/.codex/agents/kotlin-review.toml similarity index 55% rename from .claude/agents/kotlin-review.md rename to .codex/agents/kotlin-review.toml index 74583aa33b..56e491b856 100644 --- a/.claude/agents/kotlin-review.md +++ b/.codex/agents/kotlin-review.toml @@ -1,10 +1,6 @@ ---- -name: kotlin-review -description: Reviews Kotlin (and Java) changes against Spine coding guidelines, safety rules, and testing policy. Use proactively after any non-trivial code edit, before opening a PR, or when the user asks for a code review. Read-only; does not run builds. -tools: Read, Grep, Glob, Bash -model: inherit ---- - +name = "kotlin-review" +description = "Reviews Kotlin (and Java) changes against Spine coding guidelines, safety rules, and testing policy. Use proactively after any non-trivial code edit, before opening a PR, or when the user asks for a code review. Read-only; does not run builds." +developer_instructions = """ Follow the `kotlin-review` skill exactly: - Skill: `.agents/skills/kotlin-review/SKILL.md` @@ -14,4 +10,4 @@ Follow the `kotlin-review` skill exactly: - Stay in scope: code only. If a documentation issue surfaces, note it briefly as a Nit pointing at the `review-docs` agent. - Read-only: use `Read`, `Grep`, `Glob`, and `Bash` solely for `git diff` - and related read-only inspection. Do not run builds. + and related read-only inspection. Do not run builds.""" diff --git a/.claude/agents/review-docs.md b/.codex/agents/review-docs.toml similarity index 52% rename from .claude/agents/review-docs.md rename to .codex/agents/review-docs.toml index 0481b240b1..6ebbd6ad7c 100644 --- a/.claude/agents/review-docs.md +++ b/.codex/agents/review-docs.toml @@ -1,10 +1,6 @@ ---- -name: review-docs -description: Reviews documentation changes โ€” KDoc/Javadoc inside Kotlin/Java sources and Markdown docs (`README.md`, `docs/**`) โ€” against Spine documentation conventions. Use proactively when a diff touches doc comments or Markdown, before opening a doc-affecting PR, or when the user asks for a documentation review. Read-only; does not run builds. -tools: Read, Grep, Glob, Bash -model: inherit ---- - +name = "review-docs" +description = "Reviews documentation changes โ€” KDoc/Javadoc inside Kotlin/Java sources and Markdown docs (`README.md`, `docs/**`) โ€” against Spine documentation conventions. Use proactively when a diff touches doc comments or Markdown, before opening a doc-affecting PR, or when the user asks for a documentation review. Read-only; does not run builds." +developer_instructions = """ Follow the `review-docs` skill exactly: - Skill: `.agents/skills/review-docs/SKILL.md` @@ -15,4 +11,4 @@ Follow the `review-docs` skill exactly: surface it briefly as a Nit pointing at the `kotlin-review` agent โ€” do not expand the review. - Read-only: use `Read`, `Grep`, `Glob`, and `Bash` solely for `git diff` - and related read-only inspection. Do not run builds. + and related read-only inspection. Do not run builds.""" diff --git a/.codex/hooks.json b/.codex/hooks.json new file mode 100644 index 0000000000..b7e5c49296 --- /dev/null +++ b/.codex/hooks.json @@ -0,0 +1,43 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Edit|Write|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/protect-version-file.sh" + } + ] + }, + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/pre-pr-gate.sh" + }, + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/publish-version-gate.sh" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Edit|Write|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/sanitize-source-code.sh" + }, + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/update-copyright.sh" + } + ] + } + ] + } +} diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 8a5ab934a9..039657bee2 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -10,8 +10,8 @@ repository root โ€” read it first. If `.agents/project.md` exists, read it before reviewing. It provides the language, architecture, role, and code review checklist for this specific repo. -Additional guidelines are in `.agents/` โ€” see `.agents/_TOC.md` for the index -(if present; Hugo repos do not include this file). +Additional guidelines are in `.agents/guidelines/` โ€” see +`.agents/guidelines/_TOC.md` for the index. ## Do not review diff --git a/.github/workflows/build-on-ubuntu.yml b/.github/workflows/build-on-ubuntu.yml index cd6b93714c..d109aeae89 100644 --- a/.github/workflows/build-on-ubuntu.yml +++ b/.github/workflows/build-on-ubuntu.yml @@ -34,5 +34,5 @@ jobs: uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: false + fail_ci_if_error: true verbose: true diff --git a/.gitmodules b/.gitmodules index 94e8664d69..5b3352bf4e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,9 @@ [submodule "config"] path = config url = https://github.com/SpineEventEngine/config +[submodule ".agents/shared"] + path = .agents/shared + url = https://github.com/SpineEventEngine/agents.git + branch = master + update = checkout + ignore = all diff --git a/.junie/guidelines.md b/.junie/guidelines.md index 5160f499e6..7c1f866547 100644 --- a/.junie/guidelines.md +++ b/.junie/guidelines.md @@ -1,6 +1,6 @@ # Guidelines for Junie and AI Agent from JetBrains -Read the `../.agents/_TOC.md` file to understand: +Read the `../.agents/guidelines/_TOC.md` file to understand: - the agent responsibilities, - project overview, - coding guidelines, diff --git a/AGENTS.md b/AGENTS.md index f404b4ea4c..d96636dd27 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,16 +4,21 @@ If `.agents/project.md` exists in this repository, read it first โ€” it describes the language, architecture, and role of this specific repo within the Spine SDK -organisation. To create one, copy `.agents/project.template.md` (or the -relevant language template) and fill it in. If `project.md` links to a shared -requirements file (e.g. `jvm-project.md`), read that too. +organisation. It is a symlink to `docs/project.md`; to create one, copy +`.agents/guidelines/project.template.md` to `docs/project.md` and fill it in. If it +links to a shared requirements file (e.g. `jvm-project.md`), read that too. -- Start every session by reading `.agents/quick-reference-card.md` (if present). +- Start every session by reading `.agents/guidelines/quick-reference-card.md` (if present). - For specific tasks (code review, PR prep, dependency updates, docs, etc.), prefer the matching skill from `.agents/skills/`. -- Full standards reference: `.agents/_TOC.md` (if present) โ€” consult when a +- Full standards reference: `.agents/guidelines/_TOC.md` (if present) โ€” consult when a skill doesn't cover the needed context. +Shared skills, scripts, and guidelines come from the `.agents/shared` submodule (the +[`agents`][agents-repo] repository) exposed via symlinks. +`./config/pull` initializes and floats it automatically; on a fresh clone that skips +`pull`, run `git submodule update --init --remote .agents/shared`. + ## Commit and history safety **Do not commit, push, tag, rebase, merge, cherry-pick, or otherwise write to git history** @@ -26,7 +31,7 @@ unless one of the following is true *right now*: Authorization does not carry over between turns or sessions. When in doubt: stage changes, show the diff, and stop โ€” let the user commit. -See [`.agents/safety-rules.md`](.agents/safety-rules.md) โ†’ *Commits and history-writing*. +See [`.agents/guidelines/safety-rules.md`](.agents/guidelines/safety-rules.md) โ†’ *Commits and history-writing*. ## Other safety rules @@ -35,7 +40,7 @@ See [`.agents/safety-rules.md`](.agents/safety-rules.md) โ†’ *Commits and histor - No analytics, telemetry, or tracking code. - No reflection or unsafe code without explicit approval. -See [`.agents/safety-rules.md`](.agents/safety-rules.md) for the full list. +See [`.agents/guidelines/safety-rules.md`](.agents/guidelines/safety-rules.md) for the full list. ## Moving files @@ -55,6 +60,17 @@ See `.agents/memory/README.md` for layout and write protocol. Review `.agents/memory/MEMORY.md` at the start of every session. Ruthlessly iterate until mistakes stop repeating. +## Asking questions + +- Ask at most one question per message. If a decision has a small set of + options, include those options as part of that one question. +- Do not bundle unrelated clarification questions. Ask the next question only + after the user answers the previous one. +- Apply this rule both when the agent needs clarification and when the user's + prompt means "ask questions". +- Prefer a reasonable assumption over another question when the answer would not + materially change the next step. + ## Verification & Quality - Never mark a task done without proof (tests, logs, diff vs main). @@ -102,3 +118,5 @@ In consumer repositories, skip without comment any path matching: - `gradle/`, `gradlew`, `gradlew.bat` - `.codecov.yml`, `.gitignore`, `gradle.properties`, `lychee.toml` - `.github/workflows/` โ€” unless the workflow was introduced by this repo + +[agents-repo]: https://github.com/SpineEventEngine/agents diff --git a/base/src/main/java/io/spine/base/Identifier.java b/base/src/main/java/io/spine/base/Identifier.java index 7c6db84668..13ecd106f9 100644 --- a/base/src/main/java/io/spine/base/Identifier.java +++ b/base/src/main/java/io/spine/base/Identifier.java @@ -1,11 +1,11 @@ /* - * Copyright 2022, 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. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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 @@ -26,10 +26,7 @@ package io.spine.base; -import com.google.errorprone.annotations.InlineMe; import com.google.protobuf.Any; -import com.google.protobuf.Descriptors.Descriptor; -import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Int32Value; import com.google.protobuf.Int64Value; import com.google.protobuf.Message; @@ -39,7 +36,6 @@ import io.spine.string.StringifierRegistry; import org.jspecify.annotations.Nullable; -import java.util.Optional; import java.util.UUID; import static com.google.common.base.Preconditions.checkNotNull; @@ -366,26 +362,6 @@ private Any pack() { return result; } - /** - * Finds the first ID field of the specified type in the given message type. - * - * @deprecated Use {@link Field#findIdField(Class, Descriptor)} instead. - * - * @param idClass - * the class of identifiers - * @param message - * the descriptor of the message type in which to find a field - * @param - * the type of identifiers - * @return the descriptor of the matching field or - * empty {@code Optional} if there is no such a field - */ - @Deprecated - @InlineMe(replacement = "Field.findIdField(idClass, message)", imports = "io.spine.base.Field") - public static Optional findField(Class idClass, Descriptor message) { - return Field.findIdField(idClass, message); - } - @Override @SuppressWarnings("UnnecessaryDefault") // have `default` for future extensibility. public String toString() { diff --git a/base/src/main/java/io/spine/base/RejectionType.java b/base/src/main/java/io/spine/base/RejectionType.java index 2ca3fe96e6..217755002f 100644 --- a/base/src/main/java/io/spine/base/RejectionType.java +++ b/base/src/main/java/io/spine/base/RejectionType.java @@ -40,7 +40,10 @@ /** * A code generation metadata on a rejection. + * + * @deprecated This type is no longer used and will be removed in the future. */ +@Deprecated(since = "2.0.0-SNAPSHOT.392", forRemoval = true) public final class RejectionType extends MessageType { /** diff --git a/base/src/main/java/io/spine/base/UuidValue.java b/base/src/main/java/io/spine/base/UuidValue.java index 6e8ffb2442..25debbaaad 100644 --- a/base/src/main/java/io/spine/base/UuidValue.java +++ b/base/src/main/java/io/spine/base/UuidValue.java @@ -69,7 +69,7 @@ static void checkValid(String uuid) { checkNotEmptyOrBlank(uuid); try { UUID.fromString(uuid); - } catch (NumberFormatException e) { + } catch (IllegalArgumentException e) { throw newIllegalArgumentException(e, "Invalid UUID string: `%s`.", uuid); } } diff --git a/base/src/main/java/io/spine/code/fs/FsObject.java b/base/src/main/java/io/spine/code/fs/FsObject.java index b8fd73766d..6d6a4b0818 100644 --- a/base/src/main/java/io/spine/code/fs/FsObject.java +++ b/base/src/main/java/io/spine/code/fs/FsObject.java @@ -1,11 +1,11 @@ /* - * Copyright 2022, 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. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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 @@ -26,7 +26,6 @@ package io.spine.code.fs; -import com.google.errorprone.annotations.InlineMe; import org.jspecify.annotations.Nullable; import java.nio.file.Files; @@ -53,17 +52,6 @@ public final Path path() { return path; } - /** - * Obtains the directory to which this object belongs. - * - * @deprecated please use {@link #parent()}. - */ - @Deprecated - @InlineMe(replacement = "this.parent()") - public final @Nullable Path directory() { - return this.parent(); - } - /** * Obtains a parent of this file system object. */ @@ -96,6 +84,5 @@ public boolean equals(Object obj) { return false; } var other = (FsObject) obj; - return Objects.equals(this.path, other.path); - } + return Objects.equals(this.path, other.path); } } diff --git a/base/src/main/java/io/spine/code/proto/SourceFile.java b/base/src/main/java/io/spine/code/proto/SourceFile.java index 9dc5e99b58..1eb2af6650 100644 --- a/base/src/main/java/io/spine/code/proto/SourceFile.java +++ b/base/src/main/java/io/spine/code/proto/SourceFile.java @@ -1,11 +1,11 @@ /* - * Copyright 2022, 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. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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 @@ -29,9 +29,7 @@ import com.google.common.collect.ImmutableList; import com.google.protobuf.DescriptorProtos.DescriptorProto; import com.google.protobuf.Descriptors.FileDescriptor; -import io.spine.base.RejectionType; import io.spine.code.fs.AbstractSourceFile; -import io.spine.code.java.SimpleClassName; import io.spine.type.MessageType; import java.nio.file.Path; @@ -68,40 +66,6 @@ private static Path toPath(FileDescriptor file) { return result; } - /** - * Returns {@code true} if the source file matches conventions for rejection files. - * - *

A valid rejections file must: - *

    - *
  • be named ending on {@link io.spine.base.MessageFile#REJECTIONS "rejections.proto"}; - *
  • have the {@code java_multiple_files} option set to {@code false}; - *
  • either have a {@code java_outer_classname} value which ends with - * {@linkplain RejectionType#isValidOuterClassName(SimpleClassName)} โ€œRejectionsโ€}, - * or not have the {@code java_outer_classname} option set at all. - *
- * - * @deprecated Source file must not know if it matches a convention for specific types, such as - * rejections. Use {@code RejectionsFile} instead. - */ - @Deprecated - public boolean isRejections() { - // By convention, rejections are generated into one file. - if (descriptor.getOptions() - .getJavaMultipleFiles()) { - return false; - } - var outerClass = SimpleClassName.declaredOuterClassName(descriptor); - - if (outerClass.isEmpty()) { - // There's no outer class name given in options. - // Assuming the file name ends with `rejections.proto`, it's a good rejections file. - return true; - } - - var result = RejectionType.isValidOuterClassName(outerClass.get()); - return result; - } - /** * Obtains descriptor of the file. */ diff --git a/base/src/main/java/io/spine/protobuf/CollectionsConverter.java b/base/src/main/java/io/spine/protobuf/CollectionsConverter.java deleted file mode 100644 index 9d350ba66f..0000000000 --- a/base/src/main/java/io/spine/protobuf/CollectionsConverter.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2022, 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 - * - * http://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.protobuf; - -import com.google.protobuf.Any; -import io.spine.annotation.Internal; -import io.spine.base.ListOfAnys; -import io.spine.base.MapOfAnys; - -import java.util.Map; - -import static com.google.common.base.Preconditions.checkNotNull; - -/** - * Converts instances of {@code List} and {@code Map} into {@link Any}. - * - * @deprecated Use {@link TypeConverter} instead. - */ -@Internal -@Deprecated -public final class CollectionsConverter { - - /** - * Prevents this utility from instantiation. - */ - private CollectionsConverter() { - } - - /** - * Converts the passed items to {@code Any}. - * - *

This conversion is designed to preserve the ordering - * and essence of {@code Iterable} values in case this {@code Iterable} - * is required to be transferred outside JVM. - * - *

Each of the values are converted to {@code Any} one-by-one - * via {@link TypeConverter}, and then packed into a wrapping instance - * of {@link ListOfAnys}. In turn, {@code ListOfAnys} instance is packed - * into {@code Any} and returned as a result. - * - *

Please note that the types of {@code Iterable}'s values - * should be supported by {@code TypeConverter}. - * - * @param items - * the items to convert - * @return new {@code Any} instance - */ - public static Any toAny(Iterable items) { - checkNotNull(items); - var asProto = toProto(items); - var result = TypeConverter.toAny(asProto); - return result; - } - - @SuppressWarnings("ResultOfMethodCallIgnored") /* Calling `builder` methods. */ - private static ListOfAnys toProto(Iterable values) { - var builder = ListOfAnys.newBuilder(); - for (var value : values) { - builder.addValue(TypeConverter.toAny(value)); - } - return builder.build(); - } - - /** - * Converts the passed map to {@code Any}. - * - *

This conversion is designed to preserve the essence - * of the passed {@code Map} values, in case this {@code Iterable} is required - * to be transferred outside JVM. - * - *

Each of {@code Map}'s keys and values are converted one-by-one - * via {@link TypeConverter}, and then packed into a wrapping instance - * of {@link MapOfAnys}. In turn, {@code MapOfAnys} instance is packed - * into {@code Any} and returned as a result. - * - *

Please note that the types of {@code Map}'s keys and values - * should be supported by {@code TypeConverter}. - */ - public static Any toAny(Map map) { - checkNotNull(map); - var asProto = toProto(map); - var result = TypeConverter.toAny(asProto); - return result; - } - - @SuppressWarnings("ResultOfMethodCallIgnored") /* Calling `builder` methods. */ - private static MapOfAnys toProto(Map map) { - var builder = MapOfAnys.newBuilder(); - map.forEach((key, value) -> { - var entry = MapOfAnys.Entry.newBuilder() - .setKey(TypeConverter.toAny(key)) - .setValue(TypeConverter.toAny(value)); - builder.addEntry(entry); - }); - return builder.build(); - } -} diff --git a/base/src/main/java/io/spine/protobuf/Durations2.java b/base/src/main/java/io/spine/protobuf/Durations2.java index 6390fccde6..2d6e9fe67b 100644 --- a/base/src/main/java/io/spine/protobuf/Durations2.java +++ b/base/src/main/java/io/spine/protobuf/Durations2.java @@ -1,11 +1,11 @@ /* - * Copyright 2022, 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. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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 @@ -23,10 +23,10 @@ * (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.protobuf; import com.google.common.base.Converter; -import com.google.errorprone.annotations.InlineMe; import com.google.protobuf.Duration; import com.google.protobuf.util.Durations; import io.spine.string.Stringifiers; @@ -60,16 +60,6 @@ @SuppressWarnings("UtilityClass") public final class Durations2 { - /** - * Zero duration. - * - * @deprecated please use {@link Durations#ZERO}. - */ - @Deprecated - public static final Duration ZERO = fromMillis(0L); - - private static final String PROTOBUF_DURATIONS = "com.google.protobuf.util.Durations"; - /** Prevent instantiation of this utility class. */ private Durations2() { } @@ -180,19 +170,6 @@ public static boolean isPositiveOrZero(Duration value) { return result; } - /** - * Returns {@code true} if the passed value is greater than zero, - * {@code false} otherwise. - * - * @deprecated please use {@link Durations#isPositive(Duration)} - */ - @Deprecated - public static boolean isPositive(Duration value) { - checkNotNull(value); - return Durations.isPositive(value); - - } - /** Returns {@code true} if the passed value is zero, {@code false} otherwise. */ public static boolean isZero(Duration value) { checkNotNull(value); @@ -217,17 +194,6 @@ public static boolean isLessThan(Duration value, Duration another) { return result; } - /** - * Returns {@code true} if the passed duration is negative, {@code false} otherwise. - * - * @deprecated please use {@link Durations#isNegative(Duration)}. - */ - @Deprecated - @InlineMe(replacement = "Durations.isNegative(value)", imports = PROTOBUF_DURATIONS) - public static boolean isNegative(Duration value) { - return Durations.isNegative(value); - } - /** * Converts the passed Java Time value. */ diff --git a/base/src/main/java/io/spine/query/Columns.java b/base/src/main/java/io/spine/query/Columns.java index 5b410c7455..3b7067ecb9 100644 --- a/base/src/main/java/io/spine/query/Columns.java +++ b/base/src/main/java/io/spine/query/Columns.java @@ -1,11 +1,11 @@ /* - * Copyright 2022, 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. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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 @@ -180,10 +180,7 @@ public String toString() { /** * Always throws an {@link UnsupportedOperationException}. - * - * @deprecated as does not apply to this immutable type */ - @Deprecated @Override @DoNotCall("Always throws `UnsupportedOperationException`") public void replaceAll(UnaryOperator> operator) { @@ -192,10 +189,7 @@ public void replaceAll(UnaryOperator> operator) { /** * Always throws an {@link UnsupportedOperationException}. - * - * @deprecated as does not apply to this immutable type */ - @Deprecated @Override @DoNotCall("Always throws `UnsupportedOperationException`") public void sort(Comparator> c) { @@ -204,10 +198,7 @@ public void sort(Comparator> c) { /** * Always throws an {@link UnsupportedOperationException}. - * - * @deprecated as does not apply to this immutable type */ - @Deprecated @Override @DoNotCall("Always throws `UnsupportedOperationException`") public boolean removeIf(Predicate> filter) { @@ -216,10 +207,7 @@ public boolean removeIf(Predicate> filter) { /** * Always throws an {@link UnsupportedOperationException}. - * - * @deprecated as does not apply to this immutable type */ - @Deprecated @Override @DoNotCall("Always throws `UnsupportedOperationException`") public boolean add(RecordColumn column) { @@ -228,10 +216,7 @@ public boolean add(RecordColumn column) { /** * Always throws an {@link UnsupportedOperationException}. - * - * @deprecated as does not apply to this immutable type */ - @Deprecated @Override @DoNotCall("Always throws `UnsupportedOperationException`") public boolean addAll(@NonNull Collection> c) { @@ -240,10 +225,7 @@ public boolean addAll(@NonNull Collection> c) { /** * Always throws an {@link UnsupportedOperationException}. - * - * @deprecated as does not apply to this immutable type */ - @Deprecated @Override @DoNotCall("Always throws `UnsupportedOperationException`") public boolean addAll(int index, @NonNull Collection> c) { @@ -252,10 +234,7 @@ public boolean addAll(int index, @NonNull Collection element) { @@ -264,10 +243,7 @@ public void add(int index, RecordColumn element) { /** * Always throws an {@link UnsupportedOperationException}. - * - * @deprecated as does not apply to this immutable type */ - @Deprecated @Override @DoNotCall("Always throws `UnsupportedOperationException`") public RecordColumn set(int index, RecordColumn element) { @@ -276,10 +252,7 @@ public void add(int index, RecordColumn element) { /** * Always throws an {@link UnsupportedOperationException}. - * - * @deprecated as does not apply to this immutable type */ - @Deprecated @Override @DoNotCall("Always throws `UnsupportedOperationException`") public boolean remove(Object o) { @@ -288,10 +261,7 @@ public boolean remove(Object o) { /** * Always throws an {@link UnsupportedOperationException}. - * - * @deprecated as does not apply to this immutable type */ - @Deprecated @Override @DoNotCall("Always throws `UnsupportedOperationException`") public boolean removeAll(Collection c) { @@ -300,10 +270,7 @@ public boolean removeAll(Collection c) { /** * Always throws an {@link UnsupportedOperationException}. - * - * @deprecated as does not apply to this immutable type */ - @Deprecated @Override @DoNotCall("Always throws `UnsupportedOperationException`") public boolean retainAll(Collection c) { @@ -312,10 +279,7 @@ public boolean retainAll(Collection c) { /** * Always throws an {@link UnsupportedOperationException}. - * - * @deprecated as does not apply to this immutable type */ - @Deprecated @Override @DoNotCall("Always throws `UnsupportedOperationException`") public void clear() { @@ -324,10 +288,7 @@ public void clear() { /** * Always throws an {@link UnsupportedOperationException}. - * - * @deprecated as does not apply to this immutable type */ - @Deprecated @Override @DoNotCall("Always throws `UnsupportedOperationException`") public RecordColumn remove(int index) { diff --git a/base/src/main/java/io/spine/type/TypeUrl.java b/base/src/main/java/io/spine/type/TypeUrl.java index f47ec461ac..6e5538b07c 100644 --- a/base/src/main/java/io/spine/type/TypeUrl.java +++ b/base/src/main/java/io/spine/type/TypeUrl.java @@ -1,5 +1,5 @@ /* - * Copyright 2024, 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. @@ -28,7 +28,6 @@ import com.google.common.base.Splitter; import com.google.errorprone.annotations.Immutable; -import com.google.errorprone.annotations.InlineMe; import com.google.protobuf.Any; import com.google.protobuf.AnyOrBuilder; import com.google.protobuf.Descriptors.Descriptor; @@ -273,17 +272,6 @@ public String toString() { return value(); } - /** - * Converts the instance to {@code TypeName}. - * - * @deprecated Please use {@link #typeName()} and {@code typeName} in Kotlin. - */ - @Deprecated - @InlineMe(replacement = "this.typeName()") - public TypeName toTypeName() { - return typeName(); - } - /** * Obtains the type name component of this type URL. */ diff --git a/base/src/main/java/io/spine/util/Text.java b/base/src/main/java/io/spine/util/Text.java deleted file mode 100644 index 6e93eabf16..0000000000 --- a/base/src/main/java/io/spine/util/Text.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright 2022, 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 - * - * http://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.util; - -import com.google.common.base.Joiner; -import com.google.common.base.Splitter; -import com.google.common.collect.ImmutableList; -import com.google.errorprone.annotations.Immutable; - -import java.util.Iterator; -import java.util.List; -import java.util.Objects; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static io.spine.util.Exceptions.newIllegalArgumentException; -import static java.lang.System.lineSeparator; - -/** - * A text with lines separated by {@link System#lineSeparator()}. - * - * @apiNote Even though this class provides static utilities for splitting and merging - * lines, please prefer using instance methods of this class. - * @deprecated Please use a similar type for - * the Text library. - */ -@Immutable -@Deprecated -public final class Text implements Iterable { - - private static final Splitter SPLITTER = Splitter.on(lineSeparator()); - private static final Joiner JOINER = Joiner.on(lineSeparator()); - - private final ImmutableList lines; - private final String value; - - /** - * Creates a new instance with the given lines. - * - *

All the given lines must not contain a {@linkplain System#lineSeparator() - * line separator}. - * - * @throws IllegalArgumentException - * if any of the lines contains the {@linkplain System#lineSeparator() - * line separator} - */ - public Text(Iterable lines) { - checkNotNull(lines); - checkNoSeparators(lines); - this.lines = ImmutableList.copyOf(lines); - this.value = join(lines); - } - - private static void checkNoSeparators(Iterable lines) { - lines.forEach(l -> { - if (containsSeparator(l)) { - throw newIllegalArgumentException("The line contains line separator: `%s`.", l); - } - }); - } - - /** - * Creates a new instance splitting the given text into lines. - */ - public Text(String text) { - this(split(text)); - } - - /** - * Creates a new instance with the given lines. - * - *

All the given lines must not contain a {@linkplain System#lineSeparator() - * line separator}. - * - * @throws IllegalArgumentException - * if any of the lines contains the {@linkplain System#lineSeparator() - * line separator} - */ - public Text(String[] lines) { - this(ImmutableList.copyOf(lines)); - } - - /** - * Creates a new list with the given lines. - * - *

All the given lines must not contain a {@linkplain System#lineSeparator() - * line separator}. - * - * @throws IllegalArgumentException - * if any of the lines contains the {@linkplain System#lineSeparator() - * line separator} - */ - public static Text of(String... lines) { - checkNotNull(lines); - return new Text(lines); - } - - /** - * Obtains a read-only view of the text lines. - */ - public List lines() { - return lines; - } - - /** - * Obtains the text as joined lines. - * - *

This method always returns the same object, so repeated calls have no performance effect. - * - * @see #toString() - */ - public String value() { - return value; - } - - /** - * Tells if the given string is contained by any of the text lines. - * - * @param s - * the string to find. Must not contain a {@linkplain System#lineSeparator() - * line separator}. - * @return {@code true} if at least one line contains the given string, {@code false} otherwise - * @throws IllegalArgumentException - * if the given string contains the {@linkplain System#lineSeparator() - * line separator} - */ - public boolean contains(String s) { - checkArgument(!containsSeparator(s)); - var result = lines.stream().anyMatch(line -> line.contains(s)); - return result; - } - - private static boolean containsSeparator(String s) { - return s.contains(lineSeparator()); - } - - /** - * Obtains the text with joined lines separated by {@linkplain System#lineSeparator() - * line separator}. - */ - @Override - public String toString() { - return value; - } - - @Override - public Iterator iterator() { - return lines.stream().iterator(); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof Text)) { - return false; - } - var strings = (Text) o; - return Objects.equals(value, strings.value); - } - - @Override - public int hashCode() { - return value.hashCode(); - } - - /** - * Splits the passed text separated with {@linkplain System#lineSeparator() - * line separator} into lines. - * - * @return unmodifiable list of code lines - */ - public static List split(String text) { - checkNotNull(text); - return splitter().splitToList(text); - } - - /** - * Join the lines separated with {@linkplain System#lineSeparator() - * line separator}. - */ - public static String join(Iterable lines) { - checkNotNull(lines); - return joiner().join(lines); - } - - /** - * Join the lines separating them with {@linkplain System#lineSeparator() - * line separator}. - */ - public static String join(String[] lines) { - checkNotNull(lines); - return joiner().join(lines); - } - - /** - * Obtains the {@link Joiner} on {@linkplain System#lineSeparator() - * line separator}. - */ - public static Joiner joiner() { - return JOINER; - } - - /** - * Obtains the {@link Splitter} on {@link System#lineSeparator()}. - */ - public static Splitter splitter() { - return SPLITTER; - } -} diff --git a/base/src/main/kotlin/io/spine/io/Files.kt b/base/src/main/kotlin/io/spine/io/Files.kt index 1dbe1f1f30..a642ca0a2e 100644 --- a/base/src/main/kotlin/io/spine/io/Files.kt +++ b/base/src/main/kotlin/io/spine/io/Files.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. @@ -44,22 +44,6 @@ public fun File.replaceExtension(newExtension: String): File { return resolveSibling(nameWithoutExtension + newExt) } -/** - * Has no effect under Windows because file paths are always normalized. - * - * Please use [File.toUnixPath] instead. - */ -@Deprecated( - message = "Please use `toUnixPath()` instead.", - replaceWith = ReplaceWith("toUnixPath()") -) -public fun File.toUnix(): File = - if (path.contains(Separator.Windows)) { - File(path.toUnix()) - } else { - this - } - /** * Obtains the path with [Unix][Separator.Unix] separators. * diff --git a/base/src/main/kotlin/io/spine/protobuf/AnyExts.kt b/base/src/main/kotlin/io/spine/protobuf/AnyExts.kt index e8aef7ae51..061ec811bb 100644 --- a/base/src/main/kotlin/io/spine/protobuf/AnyExts.kt +++ b/base/src/main/kotlin/io/spine/protobuf/AnyExts.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. @@ -49,19 +49,6 @@ public inline fun AnyProto.unpack(): T { return AnyPacker.unpack(this, cls.java) } -/** - * Unpacks this `Any`. - * - * The concrete type of the message is looked up among the known types by - * the value of the `Any.type_url` field. - */ -@Deprecated( - message = "Please use `unpackKnownType()` instead.", - replaceWith = ReplaceWith("unpackKnownType()") -) -public fun AnyProto.unpackGuessingType(): Message = - unpackKnownType() - /** * Unpacks this `Any`. * diff --git a/base/src/main/kotlin/io/spine/string/Indent.kt b/base/src/main/kotlin/io/spine/string/Indent.kt index 9a5795740f..b1368c912b 100644 --- a/base/src/main/kotlin/io/spine/string/Indent.kt +++ b/base/src/main/kotlin/io/spine/string/Indent.kt @@ -1,11 +1,11 @@ /* - * Copyright 2023, 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. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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 @@ -53,15 +53,6 @@ public data class Indent( public companion object { - /** - * The default indentation, which is primarily used in the generated Java code. - */ - @Deprecated( - message = "Please use `DEFAULT_JAVA_INDENT_SIZE`.", - replaceWith = ReplaceWith("DEFAULT_JAVA_INDENT_SIZE") - ) - public const val DEFAULT_SIZE: Int = 4 - /** * The default size of indentation used in the Java code. */ diff --git a/base/src/main/kotlin/io/spine/util/MoreCollections.kt b/base/src/main/kotlin/io/spine/util/MoreCollections.kt deleted file mode 100644 index 51cab84b8d..0000000000 --- a/base/src/main/kotlin/io/spine/util/MoreCollections.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2025, 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 - * - * http://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.util - -import io.spine.collect.interlaced -import io.spine.collect.theOnly - -/** - * Obtains the only element in the receiver `Iterable`. - * - * @throws NoSuchElementException if the iterable is empty. - * @throws IllegalArgumentException if the iterable contains multiple elements. - */ -@Deprecated( - message = "Please use `io.spine.collect.theOnly()` instead.", - replaceWith = ReplaceWith(imports = ["io.spine.collect.theOnly"], expression = "theOnly()") -) -public fun Iterable.theOnly(): E = theOnly() - -/** - * Builds a `Sequence` which consists of the elements of this `Iterable` and - * the given [infix] between them. - * - * Example: - * - `listOf(0, 1, 2).interlaced(42)` -> `[0, 42, 1, 42, 2]`; - * - `listOf("sea", "Moon", "Earth", "Sun").interlaced("of")` -> - * `["sea", "of", "Moon", "of", "Earth", "of", "Sun"]`; - * - `listOf().interlaced("")` -> `[]`. - */ -@Deprecated( - message = "Please use `io.spine.collect.interlaced()` instead.", - replaceWith = ReplaceWith( - imports = ["io.spine.collect.interlaced"], - expression = "interlaced()" - ) -) -public fun Iterable.interlaced(infix: T): Sequence = interlaced(infix) diff --git a/base/src/test/java/io/spine/protobuf/CollectionsConverterTest.java b/base/src/test/java/io/spine/protobuf/CollectionsConverterTest.java deleted file mode 100644 index d186298aba..0000000000 --- a/base/src/test/java/io/spine/protobuf/CollectionsConverterTest.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2025, 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.protobuf; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import io.spine.base.ListOfAnys; -import io.spine.base.MapOfAnys; -import io.spine.testing.UtilityClassTest; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; -import static io.spine.protobuf.TypeConverter.toObject; - -@SuppressWarnings("deprecation") // We still tell until `CollectionsConverter` is removed. -@DisplayName("`CollectionsConverter` should") -class CollectionsConverterTest extends UtilityClassTest { - - CollectionsConverterTest() { - super(CollectionsConverter.class); - } - - @Test - @DisplayName("convert `Iterable`s to `Any`") - void convertLists() { - var source = ImmutableList.of(2, 12, 85, 6); - var result = CollectionsConverter.toAny(source); - assertThat(result) - .isNotNull(); - var unpacked = AnyPacker.unpack(result); - assertThat(unpacked) - .isInstanceOf(ListOfAnys.class); - var listOfAnys = (ListOfAnys) unpacked; - var values = listOfAnys.getValueList(); - for (var index = 0; index < values.size(); index++) { - var element = values.get(index); - var unpackedElement = toObject(element, Integer.class); - assertThat(unpackedElement) - .isEqualTo(source.get(index)); - } - } - - @Test - @DisplayName("convert `Map`s to `Any`") - void convertMaps() { - var source = ImmutableMap.of("first", 1, - "second", 2, - "third", 3); - var result = CollectionsConverter.toAny(source); - assertThat(result) - .isNotNull(); - var unpacked = AnyPacker.unpack(result); - assertThat(unpacked) - .isInstanceOf(MapOfAnys.class); - var mapOfAnys = (MapOfAnys) unpacked; - var entries = mapOfAnys.getEntryList(); - assertThat(entries.size()) - .isEqualTo(source.size()); - for (var entry : entries) { - var packedKey = entry.getKey(); - var key = toObject(packedKey, String.class); - var packedValue = entry.getValue(); - var value = toObject(packedValue, Integer.class); - assertThat(source.keySet()) - .contains(key); - assertThat(value) - .isEqualTo(source.get(key)); - } - } -} diff --git a/base/src/test/kotlin/io/spine/base/MistakeSpec.kt b/base/src/test/kotlin/io/spine/base/MistakeSpec.kt index e4474c1668..f0960b71b5 100644 --- a/base/src/test/kotlin/io/spine/base/MistakeSpec.kt +++ b/base/src/test/kotlin/io/spine/base/MistakeSpec.kt @@ -70,10 +70,11 @@ internal class MistakeSpec { } } -private class KMistake(message: String?, cause: Throwable?) : Mistake(message, cause) { - constructor(message: String?) : this(message, null) - constructor(cause: Throwable?) : this(cause?.toString(), cause) - constructor() : this(null, null) +private class KMistake : Mistake { + constructor() : super() + constructor(message: String?) : super(message) + constructor(cause: Throwable?) : super(cause) + constructor(message: String?, cause: Throwable?) : super(message, cause) companion object { private const val serialVersionUID: Long = 0L diff --git a/base/src/test/kotlin/io/spine/base/RejectionTypeKtSpec.kt b/base/src/test/kotlin/io/spine/base/RejectionTypeKtSpec.kt new file mode 100644 index 0000000000..48e1585b8d --- /dev/null +++ b/base/src/test/kotlin/io/spine/base/RejectionTypeKtSpec.kt @@ -0,0 +1,53 @@ +/* + * 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.base + +import io.kotest.matchers.shouldBe +import io.spine.code.java.SimpleClassName +import io.spine.test.base.rejections.TestRejections.FlyingObjectUnidentified +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`RejectionType` should") +class RejectionTypeKtSpec { + + @Test + fun `recognize a rejection type`() { + val descriptor = FlyingObjectUnidentified.getDescriptor() + RejectionType.test(descriptor) shouldBe true + + val type = RejectionType(descriptor) + type.throwableClass().value() shouldBe + "io.spine.test.base.rejections.FlyingObjectUnidentified" + } + + @Test + fun `verify outer class name`() { + RejectionType.isValidOuterClassName(SimpleClassName.create("MyRejections")) shouldBe true + RejectionType.isValidOuterClassName(SimpleClassName.create("MyEvents")) shouldBe false + } +} diff --git a/base/src/test/kotlin/io/spine/base/ThrowableExtsTest.kt b/base/src/test/kotlin/io/spine/base/ThrowableExtsTest.kt index a1ae11dc75..8bb649a283 100644 --- a/base/src/test/kotlin/io/spine/base/ThrowableExtsTest.kt +++ b/base/src/test/kotlin/io/spine/base/ThrowableExtsTest.kt @@ -42,8 +42,8 @@ import org.junit.jupiter.api.Test internal class `Extensions for 'Throwable' should` { - @Nested - inner class `tell if it was caused by a rejection` { + @Nested inner class + `tell if it was caused by a rejection` { private var throwable: Throwable = RuntimeException() diff --git a/base/src/test/kotlin/io/spine/base/UuidValueSpec.kt b/base/src/test/kotlin/io/spine/base/UuidValueSpec.kt index 1a68d803ea..778d66d1f9 100644 --- a/base/src/test/kotlin/io/spine/base/UuidValueSpec.kt +++ b/base/src/test/kotlin/io/spine/base/UuidValueSpec.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024, 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. @@ -26,25 +26,37 @@ package io.spine.base -import java.util.UUID +import io.kotest.assertions.throwables.shouldThrow import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertDoesNotThrow -import org.junit.jupiter.api.assertThrows +import java.util.UUID @DisplayName("`UuidValue` should") -internal class UuidValueSpec { +class UuidValueSpec { + + @Test + fun `validate UUID string`() { + UuidValue.checkValid(UUID.randomUUID().toString()) + } @Test - fun `provide validation method for a string value`() { - assertThrows { + fun `fail on empty string`() { + shouldThrow { UuidValue.checkValid("") } - assertThrows { - UuidValue.checkValid("1-2-3") + } + + @Test + fun `fail on blank string`() { + shouldThrow { + UuidValue.checkValid(" ") } - assertDoesNotThrow { - UuidValue.checkValid(UUID.randomUUID().toString()) + } + + @Test + fun `fail on invalid UUID string`() { + shouldThrow { + UuidValue.checkValid("not-a-uuid") } } } diff --git a/base/src/test/kotlin/io/spine/code/fs/AbstractSourceFileKtSpec.kt b/base/src/test/kotlin/io/spine/code/fs/AbstractSourceFileKtSpec.kt new file mode 100644 index 0000000000..0962df5dda --- /dev/null +++ b/base/src/test/kotlin/io/spine/code/fs/AbstractSourceFileKtSpec.kt @@ -0,0 +1,77 @@ +/* + * 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.code.fs + +import com.google.common.collect.ImmutableList +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldContainExactly +import java.nio.file.Files +import java.nio.file.Path +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +@DisplayName("`AbstractSourceFile` should") +class AbstractSourceFileKtSpec { + + private class StubSourceFile(path: Path) : AbstractSourceFile(path) { + public override fun load() = super.load() + public override fun store() = super.store() + public fun setLines(lines: List) = update(ImmutableList.copyOf(lines)) + public fun getLines() = lines() + } + + @Test + fun `return empty lines if not loaded`(@TempDir tempDir: Path) { + val file = StubSourceFile(tempDir.resolve("non-existent")) + file.getLines().shouldBeEmpty() + } + + @Test + fun `load lines from file`(@TempDir tempDir: Path) { + val path = tempDir.resolve("test.txt") + val content = listOf("line 1", "line 2") + Files.write(path, content) + + val file = StubSourceFile(path) + file.load() + file.getLines() shouldContainExactly content + } + + @Test + fun `store lines to file`(@TempDir tempDir: Path) { + val path = tempDir.resolve("test.txt") + Files.createFile(path) + val content = listOf("line 1", "line 2") + + val file = StubSourceFile(path) + file.setLines(content) + file.store() + + Files.readAllLines(path) shouldContainExactly content + } +} diff --git a/base/src/test/kotlin/io/spine/code/fs/FsObjectSpec.kt b/base/src/test/kotlin/io/spine/code/fs/FsObjectSpec.kt new file mode 100644 index 0000000000..2ecdfb2826 --- /dev/null +++ b/base/src/test/kotlin/io/spine/code/fs/FsObjectSpec.kt @@ -0,0 +1,105 @@ +/* + * 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.code.fs + +import com.google.common.testing.EqualsTester +import io.kotest.matchers.shouldBe +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +@DisplayName("`FsObject` should") +class FsObjectSpec { + + private class StubFsObject(path: Path) : FsObject(path) + private class StubSourceCodeDir(path: Path) : SourceCodeDirectory(path) + private class StubSourceFile(path: Path) : AbstractSourceFile(path) + + @Test + fun `expose path`(@TempDir tempDir: Path) { + val path = tempDir.resolve("some-file") + val obj = StubFsObject(path) + obj.path() shouldBe path + } + + @Test + fun `expose parent`(@TempDir tempDir: Path) { + val path = tempDir.resolve("parent/child") + val obj = StubFsObject(path) + obj.parent() shouldBe path.parent + } + + @Test + fun `tell if exists`(@TempDir tempDir: Path) { + val path = tempDir.resolve("real-file") + Files.createFile(path) + val obj = StubFsObject(path) + obj.exists() shouldBe true + + val missing = tempDir.resolve("missing-file") + StubFsObject(missing).exists() shouldBe false + } + + @Test + fun `provide 'toString'`() { + val path = Paths.get("some", "path") + val obj = StubFsObject(path) + obj.toString() shouldBe path.toString() + } + + @Test + fun `support equality`() { + val path1 = Paths.get("p1") + val path2 = Paths.get("p2") + EqualsTester() + .addEqualityGroup(StubFsObject(path1), StubFsObject(path1)) + .addEqualityGroup(StubFsObject(path2)) + .testEquals() + } + + @Nested internal inner class + `Source code directory` { + + @Test + fun `resolve child directory`() { + val root = StubSourceCodeDir(Paths.get("root")) + val child = StubSourceCodeDir(Paths.get("child")) + root.resolve(child) shouldBe Paths.get("root", "child") + } + + @Test + fun `resolve source file`() { + val root = StubSourceCodeDir(Paths.get("root")) + val file = StubSourceFile(Paths.get("File.java")) + root.resolve(file) shouldBe Paths.get("root", "File.java") + } + } +} diff --git a/base/src/test/kotlin/io/spine/code/proto/FieldContextKtSpec.kt b/base/src/test/kotlin/io/spine/code/proto/FieldContextKtSpec.kt new file mode 100644 index 0000000000..d46195c494 --- /dev/null +++ b/base/src/test/kotlin/io/spine/code/proto/FieldContextKtSpec.kt @@ -0,0 +1,74 @@ +/* + * 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.code.proto + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.spine.test.base.rejections.TestRejections.FlyingObjectUnidentified +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`FieldContext` should") +class FieldContextKtSpec { + + @Test + fun `create an empty context`() { + val context = FieldContext.empty() + context shouldNotBe null + context.fieldPath().fieldNameList.size shouldBe 0 + } + + @Test + fun `create context for a field`() { + val field = FlyingObjectUnidentified.getDescriptor().findFieldByName("plus_code") + val context = FieldContext.create(field) + + context.target() shouldBe field + context.fieldPath().fieldNameList.size shouldBe 1 + context.fieldPath().getFieldName(0) shouldBe "plus_code" + } + + @Test + fun `create context for a child field`() { + val field = FlyingObjectUnidentified.getDescriptor().findFieldByName("plus_code") + val parent = FieldContext.empty() + val child = parent.forChild(field) + + child.target() shouldBe field + child.fieldPath().fieldNameList.size shouldBe 1 + } + + @Test + fun `compare contexts`() { + val field = FlyingObjectUnidentified.getDescriptor().findFieldByName("plus_code") + val c1 = FieldContext.create(field) + val c2 = FieldContext.create(field) + + c1 shouldBe c2 + c1.hashCode() shouldBe c2.hashCode() + } +} diff --git a/base/src/test/kotlin/io/spine/code/proto/FieldDeclarationSpec.kt b/base/src/test/kotlin/io/spine/code/proto/FieldDeclarationSpec.kt index 938c68878c..54b64592cc 100644 --- a/base/src/test/kotlin/io/spine/code/proto/FieldDeclarationSpec.kt +++ b/base/src/test/kotlin/io/spine/code/proto/FieldDeclarationSpec.kt @@ -52,6 +52,7 @@ import org.junit.jupiter.api.Test @Suppress("TooManyFunctions") @DisplayName("`FieldDeclaration` should") internal class FieldDeclarationSpec { + @Test fun `not accept 'null's on construction`() { val descriptor = Any.getDescriptor() @@ -86,9 +87,8 @@ internal class FieldDeclarationSpec { .testEquals() } - @Nested - @DisplayName("check default values of type") - internal inner class Defaults { + @Nested internal inner class + `check default values of type` { @Test fun int32() { @@ -101,8 +101,7 @@ internal class FieldDeclarationSpec { } @Test - @DisplayName("`string`") - fun aString() { + fun `string`() { val stringField = StringValue.getDescriptor().fields[0] val declaration = FieldDeclaration(stringField) @@ -111,8 +110,7 @@ internal class FieldDeclarationSpec { } @Test - @DisplayName("`Message`") - fun aMessage() { + fun `Message`() { val messageField = Uri.getDescriptor().findFieldByName("auth") val declaration = FieldDeclaration(messageField) @@ -122,9 +120,8 @@ internal class FieldDeclarationSpec { } } - @Nested - @DisplayName("obtain Java type name of") - internal inner class TypeName { + @Nested internal inner class + `obtain Java type name of` { @Test fun int64() { @@ -145,8 +142,7 @@ internal class FieldDeclarationSpec { } @Test - @DisplayName("`Message`") - fun message() { + fun `Message`() { val messageField = Uri.getDescriptor().findFieldByName("protocol") val declaration = FieldDeclaration(messageField) val typeName = declaration.javaTypeName() @@ -155,8 +151,7 @@ internal class FieldDeclarationSpec { } @Test - @DisplayName("`enum`") - fun anEnum() { + fun `enum`() { val enumField = Uri.Protocol.getDescriptor().findFieldByName("schema") val declaration = FieldDeclaration(enumField) val typeName = declaration.javaTypeName() diff --git a/base/src/test/kotlin/io/spine/code/proto/FileNameSpec.kt b/base/src/test/kotlin/io/spine/code/proto/FileNameSpec.kt index f4419b0150..efd1b54e61 100644 --- a/base/src/test/kotlin/io/spine/code/proto/FileNameSpec.kt +++ b/base/src/test/kotlin/io/spine/code/proto/FileNameSpec.kt @@ -63,9 +63,8 @@ internal class FileNameSpec { FileName.of("many_more_rejections.proto").nameOnlyCamelCase() shouldBe "ManyMoreRejections" } - @Nested - @DisplayName("Calculate outer class name") - internal inner class OuterClassName { + @Nested internal inner class + `calculating outer class name` { @Test fun `one word name`() { @@ -104,8 +103,7 @@ internal class FileNameSpec { } @Test - @DisplayName("tell events file kind") - fun eventsFile() { + fun `tell events file kind`() { val eventsFile = FileName.of("project_events.proto") eventsFile.isEvents shouldBe true @@ -114,8 +112,7 @@ internal class FileNameSpec { } @Test - @DisplayName("tell rejection file kind") - fun rejectionsFile() { + fun `tell rejection file kind`() { val rejectionsFile = FileName.of("rejections.proto") rejectionsFile.isRejections shouldBe true diff --git a/base/src/test/kotlin/io/spine/code/proto/FileSetSpec.kt b/base/src/test/kotlin/io/spine/code/proto/FileSetSpec.kt new file mode 100644 index 0000000000..f5d88182e5 --- /dev/null +++ b/base/src/test/kotlin/io/spine/code/proto/FileSetSpec.kt @@ -0,0 +1,155 @@ +/* + * Copyright 2025, 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.code.proto + +import com.google.common.testing.EqualsTester +import com.google.protobuf.DescriptorProtos.FileDescriptorProto +import com.google.protobuf.Empty +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.ints.shouldBeLessThan +import io.kotest.matchers.optional.shouldBePresent +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.shouldContain +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +@DisplayName("`FileSet` should") +internal class FileSetSpec { + + private lateinit var fileSet: FileSet + + @BeforeEach + fun loadFileSet() { + fileSet = FileSet.load() + } + + @Nested internal inner class + `verify if it contains a file` { + + @Test + fun `by its name`() { + val name = FileName.from(Empty.getDescriptor().file) + fileSet.contains(name) shouldBe true + } + + @Test + fun `by a collection of names`() { + val name = FileName.from(Empty.getDescriptor().file) + fileSet.containsAll(listOf(name)) shouldBe true + } + } + + @Nested internal inner class + `find a file` { + + @Test + fun `by its name`() { + val name = FileName.from(Empty.getDescriptor().file) + fileSet.tryFind(name).shouldBePresent() + } + + @Test + fun `by a collection of names`() { + val name = FileName.from(Empty.getDescriptor().file) + val found = fileSet.find(listOf(name)) + found.size() shouldBe 1 + } + } + + @Test + fun `be empty when newly created`() { + val emptySet = FileSet.newInstance() + emptySet.isEmpty shouldBe true + emptySet.size() shouldBe 0 + } + + @Test + fun `create a union with another set`() { + val emptySet = FileSet.newInstance() + fileSet.union(emptySet) shouldBe fileSet + emptySet.union(fileSet) shouldBe fileSet + + val anotherSet = FileSet.newInstance() + val file = Empty.getDescriptor().file + anotherSet.add(file) + + val union = emptySet.union(anotherSet) + union.size() shouldBe 1 + union.contains(FileName.from(file)) shouldBe true + } + + @Test + fun `filter files by predicate`() { + val filtered = fileSet.filter { it.fullName.contains("empty") } + filtered.files().forEach { + it.fullName.contains("empty") shouldBe true + } + } + + @Test + fun `convert to array`() { + val array = fileSet.toArray() + array.size shouldBe fileSet.size() + } + + @Test + fun `support 'equals()' and 'hashCode()'`() { + val file = Empty.getDescriptor().file + val set1 = FileSet.newInstance() + set1.add(file) + val set2 = FileSet.newInstance() + set2.add(file) + + EqualsTester() + .addEqualityGroup(set1, set2) + .addEqualityGroup(FileSet.newInstance()) + .addEqualityGroup(fileSet) + .testEquals() + } + + @Test + fun `provide 'toString'`() { + val file1 = Empty.getDescriptor().file + val file2 = com.google.protobuf.Any.getDescriptor().file + val set = FileSet.newInstance() + set.add(file1) + set.add(file2) + + val str = set.toString() + str shouldContain "FileSet" + str shouldContain "files=" + str shouldContain file1.fullName + str shouldContain file2.fullName + + // google/protobuf/any.proto comes before google/protobuf/empty.proto + str.indexOf(file2.fullName) shouldBeLessThan str.indexOf(file1.fullName) + } +} diff --git a/base/src/test/kotlin/io/spine/code/proto/MessageTypeSpec.kt b/base/src/test/kotlin/io/spine/code/proto/MessageTypeSpec.kt index c5c8305512..97cac96376 100644 --- a/base/src/test/kotlin/io/spine/code/proto/MessageTypeSpec.kt +++ b/base/src/test/kotlin/io/spine/code/proto/MessageTypeSpec.kt @@ -60,9 +60,8 @@ import org.junit.jupiter.api.Test @DisplayName("`MessageType` should") internal class MessageTypeSpec { - @Nested - @DisplayName("tell if a type") - internal inner class Tell { + @Nested internal inner class + `tell if a type` { /** * Tests a certain boolean method of `MessageType` created on the passed descriptor. @@ -73,8 +72,8 @@ internal class MessageTypeSpec { result shouldBe true } - @Nested - internal inner class Is { + @Nested internal inner class + `be` { @Test fun nested() { @@ -117,9 +116,8 @@ internal class MessageTypeSpec { * This test suite takes nested types of corresponding signals to * verify that they are not seen as signals of the kind of the enclosing types. */ - @Nested - @DisplayName("not") - internal inner class NotA { + @Nested internal inner class + `not` { @Test fun `a rejection`() { @@ -148,9 +146,8 @@ internal class MessageTypeSpec { } } - @Nested - @DisplayName("a non-Google or a Spine options type") - internal inner class Custom { + @Nested internal inner class + `a non-Google or a Spine options type` { @Test fun `positively for a custom type`() { @@ -172,9 +169,8 @@ internal class MessageTypeSpec { } } - @Nested - @DisplayName("obtain a path for") - internal inner class Path { + @Nested internal inner class + `obtain a path for` { @CanIgnoreReturnValue private fun assertPath(descriptor: Descriptor): IterableSubject { diff --git a/base/src/test/kotlin/io/spine/code/proto/PackageNameSpec.kt b/base/src/test/kotlin/io/spine/code/proto/PackageNameSpec.kt index 610d2834fb..7fdfff2699 100644 --- a/base/src/test/kotlin/io/spine/code/proto/PackageNameSpec.kt +++ b/base/src/test/kotlin/io/spine/code/proto/PackageNameSpec.kt @@ -38,7 +38,7 @@ import org.junit.jupiter.api.Test internal class PackageNameSpec { @Test - fun handleNullArgs() { + fun `handle 'null' arguments`() { NullPointerTester().testAllPublicStaticMethods(PackageName::class.java) } @@ -53,9 +53,8 @@ internal class PackageNameSpec { PackageName.of(packageName).value() shouldBe packageName } - @Nested - @DisplayName("verify if the package is inner to a parent package") - internal inner class SubPackage { + @Nested internal inner class + `verify if the package is inner to a parent package` { @Test fun `if immediately nested`() { diff --git a/base/src/test/kotlin/io/spine/code/proto/TypeSetKtSpec.kt b/base/src/test/kotlin/io/spine/code/proto/TypeSetKtSpec.kt new file mode 100644 index 0000000000..7e55db872f --- /dev/null +++ b/base/src/test/kotlin/io/spine/code/proto/TypeSetKtSpec.kt @@ -0,0 +1,62 @@ +/* + * 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.code.proto + +import io.kotest.matchers.shouldBe +import io.spine.test.base.rejections.TestRejections.FlyingObjectUnidentified +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`TypeSet` should") +class TypeSetKtSpec { + + @Test + fun `create an empty set`() { + val set = TypeSet.newBuilder().build() + set.isEmpty() shouldBe true + set.size() shouldBe 0 + } + + @Test + fun `create from a file descriptor`() { + val descriptor = FlyingObjectUnidentified.getDescriptor().file + val set = TypeSet.from(descriptor) + + set.isEmpty() shouldBe false + set.messageTypes().size shouldBe 1 + } + + @Test + fun `unite sets`() { + val descriptor = FlyingObjectUnidentified.getDescriptor().file + val set1 = TypeSet.from(descriptor) + val set2 = TypeSet.newBuilder().build() + val union = set1.union(set2) + + union shouldBe set1 + } +} diff --git a/base/src/test/kotlin/io/spine/io/CopyTest.kt b/base/src/test/kotlin/io/spine/io/CopySpec.kt similarity index 96% rename from base/src/test/kotlin/io/spine/io/CopyTest.kt rename to base/src/test/kotlin/io/spine/io/CopySpec.kt index 9423d2adc6..8ea2b8cbc1 100644 --- a/base/src/test/kotlin/io/spine/io/CopyTest.kt +++ b/base/src/test/kotlin/io/spine/io/CopySpec.kt @@ -26,8 +26,10 @@ package io.spine.io import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe import io.spine.io.Copy.copyContent import io.spine.io.Copy.copyDir +import io.spine.testing.UtilityClassTest import java.nio.file.Files.createDirectory import java.nio.file.Files.exists import java.nio.file.Files.write @@ -36,10 +38,12 @@ import java.nio.file.StandardOpenOption.CREATE import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir -class `Copy utilities should` { +@DisplayName("`Copy` utilities should") +internal class CopySpec : UtilityClassTest(Copy::class.java) { companion object { const val sourceDirectory = "to-copy" diff --git a/base/src/test/kotlin/io/spine/io/EnsureSpec.kt b/base/src/test/kotlin/io/spine/io/EnsureSpec.kt index d502baa994..eea2ae369d 100644 --- a/base/src/test/kotlin/io/spine/io/EnsureSpec.kt +++ b/base/src/test/kotlin/io/spine/io/EnsureSpec.kt @@ -44,9 +44,8 @@ import org.junit.jupiter.api.io.TempDir @DisplayName("`Ensure` utilities class should") internal class EnsureSpec : UtilityClassTest(Ensure::class.java) { - @Nested - @DisplayName("handle files via") - internal inner class OnFiles { + @Nested internal inner class + `handle files via` { private lateinit var file: File @@ -74,9 +73,8 @@ internal class EnsureSpec : UtilityClassTest(Ensure::class.java) { } } - @Nested - @DisplayName("handle a directory creation") - internal inner class OnDirectories { + @Nested internal inner class + `handle a directory creation` { private lateinit var tempDir: Path @@ -102,8 +100,7 @@ internal class EnsureSpec : UtilityClassTest(Ensure::class.java) { } @Test - @DisplayName("if it exists") - fun existing() { + fun `if it exists`() { val existingDir = tempDir.resolve(TestValues.randomString()) ensureDirectory(existingDir) diff --git a/base/src/test/kotlin/io/spine/io/Files2Spec.kt b/base/src/test/kotlin/io/spine/io/Files2Spec.kt index 13f07d2de5..9b42062fe7 100644 --- a/base/src/test/kotlin/io/spine/io/Files2Spec.kt +++ b/base/src/test/kotlin/io/spine/io/Files2Spec.kt @@ -49,9 +49,8 @@ internal class Files2Spec : UtilityClassTest(Files2::class.java) { testFolder = testFolderPath.toFile() } - @Nested - @DisplayName("verify that an existing file is not empty") - internal inner class NonEmptyFile { + @Nested internal inner class + `verify that an existing file is not empty` { @Test fun `returning 'false' when existing file is empty`() { diff --git a/base/src/test/kotlin/io/spine/io/FilesSpec.kt b/base/src/test/kotlin/io/spine/io/FilesSpec.kt index b24bd73a3f..5fa1b749c0 100644 --- a/base/src/test/kotlin/io/spine/io/FilesSpec.kt +++ b/base/src/test/kotlin/io/spine/io/FilesSpec.kt @@ -45,4 +45,10 @@ internal class FilesSpec { File("file.txt").replaceExtension("") shouldBe File("file") File("file.").replaceExtension("") shouldBe File("file") } + + @Test + fun `convert to Unix-style path`() { + File("my\\windows\\path").toUnixPath() shouldBe "my/windows/path" + File("my/unix/path").toUnixPath() shouldBe "my/unix/path" + } } diff --git a/base/src/test/kotlin/io/spine/io/GlobSpec.kt b/base/src/test/kotlin/io/spine/io/GlobSpec.kt index 972d385479..a493ce0501 100644 --- a/base/src/test/kotlin/io/spine/io/GlobSpec.kt +++ b/base/src/test/kotlin/io/spine/io/GlobSpec.kt @@ -42,8 +42,8 @@ class GlobSpec { assertThrows { Glob("") } } - @Nested - inner class `create instances by extension which` { + @Nested inner class + `create instances by extension which` { @Test fun `is empty`() { diff --git a/base/src/test/kotlin/io/spine/io/PathsSpec.kt b/base/src/test/kotlin/io/spine/io/PathsSpec.kt index cb115e3f66..b3dd79a13e 100644 --- a/base/src/test/kotlin/io/spine/io/PathsSpec.kt +++ b/base/src/test/kotlin/io/spine/io/PathsSpec.kt @@ -34,6 +34,7 @@ import java.io.File import java.nio.file.Paths import kotlin.io.path.Path import kotlin.io.path.div +import kotlin.io.path.pathString import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -71,4 +72,12 @@ internal class PathsSpec { val path = "/my/unix/path" path.toUnix() shouldBeSameInstanceAs path } + + @Test + fun `convert 'Path' to Unix separators`() { + Path("my\\windows\\path").toUnix().pathString shouldBe "my/windows/path" + + val unixPath = Path("my/unix/path") + unixPath.toUnix() shouldBeSameInstanceAs unixPath + } } diff --git a/base/src/test/kotlin/io/spine/io/ResourceDirectoryTest.kt b/base/src/test/kotlin/io/spine/io/ResourceDirectorySpec.kt similarity index 97% rename from base/src/test/kotlin/io/spine/io/ResourceDirectoryTest.kt rename to base/src/test/kotlin/io/spine/io/ResourceDirectorySpec.kt index febe146d93..dd61717b6f 100644 --- a/base/src/test/kotlin/io/spine/io/ResourceDirectoryTest.kt +++ b/base/src/test/kotlin/io/spine/io/ResourceDirectorySpec.kt @@ -34,10 +34,12 @@ import java.util.function.Predicate import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir -class `'ResourceDirectory' should` { +@DisplayName("`ResourceDirectory` should") +internal class ResourceDirectorySpec { private lateinit var directory: ResourceDirectory private lateinit var target: Path diff --git a/base/src/test/kotlin/io/spine/protobuf/DescriptorExtsSpec.kt b/base/src/test/kotlin/io/spine/protobuf/DescriptorExtsSpec.kt index 43dd18a202..054052dd01 100644 --- a/base/src/test/kotlin/io/spine/protobuf/DescriptorExtsSpec.kt +++ b/base/src/test/kotlin/io/spine/protobuf/DescriptorExtsSpec.kt @@ -42,9 +42,8 @@ import org.junit.jupiter.api.TestInstance.Lifecycle @DisplayName("`Descriptor` extensions from `io.spine.protobuf` should") internal class DescriptorExtsSpec { - @Nested - @DisplayName("provide a field descriptor by") - inner class FieldDescriptor { + @Nested inner class + `provide a field descriptor by` { private val descriptor = Timestamp.getDescriptor() diff --git a/base/src/test/kotlin/io/spine/protobuf/DiffKtSpec.kt b/base/src/test/kotlin/io/spine/protobuf/DiffKtSpec.kt new file mode 100644 index 0000000000..c55276c6e1 --- /dev/null +++ b/base/src/test/kotlin/io/spine/protobuf/DiffKtSpec.kt @@ -0,0 +1,78 @@ +/* + * 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.protobuf + +import io.kotest.matchers.shouldBe +import io.spine.code.proto.FieldDeclaration +import io.spine.test.base.rejections.flyingObjectUnidentified +import io.spine.test.base.rejections.TestRejections.FlyingObjectUnidentified +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +@DisplayName("`Diff` should") +class DiffKtSpec { + + @Test + fun `calculate difference between two messages of the same type`() { + val m1 = flyingObjectUnidentified { + plusCode = "ABC" + } + val m2 = flyingObjectUnidentified { + plusCode = "DEF" + } + + val diff = Diff.between(m1, m2) + val field = FieldDeclaration(FlyingObjectUnidentified.getDescriptor() + .findFieldByName("plus_code")) + + diff.contains(field) shouldBe true + } + + @Test + fun `return empty diff for same messages`() { + val m1 = flyingObjectUnidentified { + plusCode = "ABC" + } + + val diff = Diff.between(m1, m1) + val field = FieldDeclaration(FlyingObjectUnidentified.getDescriptor() + .findFieldByName("plus_code")) + + diff.contains(field) shouldBe false + } + + @Test + fun `throw IAE if message types are different`() { + val m1 = FlyingObjectUnidentified.getDefaultInstance() + val m2 = io.spine.base.Error.getDefaultInstance() + + assertThrows { + Diff.between(m1, m2) + } + } +} diff --git a/base/src/test/kotlin/io/spine/protobuf/Durations2Spec.kt b/base/src/test/kotlin/io/spine/protobuf/Durations2Spec.kt index 8b7bd50d63..d64fdbba8e 100644 --- a/base/src/test/kotlin/io/spine/protobuf/Durations2Spec.kt +++ b/base/src/test/kotlin/io/spine/protobuf/Durations2Spec.kt @@ -86,9 +86,8 @@ internal class Durations2Spec : UtilityClassTest(Durations2::class.j toJavaTime(original) shouldBe backward.convert(original) } - @Nested - @DisplayName("provide DSL methods for") - internal inner class Dsl { + @Nested internal inner class + `provide DSL methods for` { private var value: Long = 0 @@ -129,9 +128,8 @@ internal class Durations2Spec : UtilityClassTest(Durations2::class.j } } - @Nested - @DisplayName("convert a number of hours") - internal inner class HourConversion { + @Nested internal inner class + `convert a number of hours` { private fun test(hours: Long) { val expected = seconds(hoursToSeconds(hours)) @@ -156,9 +154,8 @@ internal class Durations2Spec : UtilityClassTest(Durations2::class.j } } - @Nested - @DisplayName("fail if") - internal inner class MathError { + @Nested internal inner class + `fail if` { @Test fun `hours value is too big`() { @@ -175,19 +172,16 @@ internal class Durations2Spec : UtilityClassTest(Durations2::class.j } } - @Nested - @DisplayName("add") - internal inner class Add { + @Nested internal inner class + `add` { @Test - @DisplayName("two `null`s -> `ZERO`") - fun nullPlusNull() { + fun `two 'null's to 'ZERO'`() { add(null, null) shouldBe Durations.ZERO } @Test - @DisplayName("`null` returning same instance") - fun sameWithNull() { + fun `'null' returning same instance`() { val duration = seconds(525) add(duration, null) shouldBeSameInstanceAs duration @@ -221,9 +215,8 @@ internal class Durations2Spec : UtilityClassTest(Durations2::class.j } } - @Nested - @DisplayName("Obtain from `Duration`") - internal inner class Obtain { + @Nested internal inner class + `Obtain from 'Duration'` { @Test fun `amount of hours`() { @@ -233,9 +226,8 @@ internal class Durations2Spec : UtilityClassTest(Durations2::class.j } } - @Nested - @DisplayName("verify if `Duration` is") - internal inner class Verify { + @Nested internal inner class + `verify if 'Duration' is` { @Test fun `positive or zero`() { @@ -252,9 +244,8 @@ internal class Durations2Spec : UtilityClassTest(Durations2::class.j } } - @Nested - @DisplayName("tell if `Duration` is") - internal inner class Compare { + @Nested internal inner class + `tell if 'Duration' is` { @Test fun greater() { diff --git a/base/src/test/kotlin/io/spine/query/CartesianProductsSpec.kt b/base/src/test/kotlin/io/spine/query/CartesianProductsSpec.kt new file mode 100644 index 0000000000..248bef3cdb --- /dev/null +++ b/base/src/test/kotlin/io/spine/query/CartesianProductsSpec.kt @@ -0,0 +1,167 @@ +/* + * Copyright 2025, 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.query + +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.spine.query.ComparisonOperator +import io.spine.query.given.RecordQueryBuilderTestEnv.ManufacturerColumns.is_traded +import io.spine.query.given.RecordQueryBuilderTestEnv.ManufacturerColumns.stock_count +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +@DisplayName("`CartesianProducts` should") +internal class CartesianProductsSpec { + + @Nested internal inner class + `multiply simple parameters over an 'OrExpression'` { + + @Test + fun `producing a union of conjunctions`() { + val param = RecordSubjectParameter(is_traded, ComparisonOperator.EQUALS, true) + val params = listOf(param) + + val customColumn = object : CustomColumn() { + override fun name(): ColumnName = ColumnName.of("custom") + override fun type(): Class = String::class.java + override fun valueIn(source: Manufacturer): String = "value" + } + val customParam = + CustomSubjectParameter(customColumn, "value", ComparisonOperator.EQUALS) + + val orExpression = OrExpression.newBuilder() + .addParam(RecordSubjectParameter(stock_count, ComparisonOperator.EQUALS, 10)) + .addParam(RecordSubjectParameter(stock_count, ComparisonOperator.EQUALS, 20)) + .addCustomParam(customParam) + .addExpression(AndExpression.newBuilder() + .addParam(RecordSubjectParameter(stock_count, ComparisonOperator.EQUALS, 30)) + .build()) + .build() + + val result = OrExpression.newBuilder() + CartesianProducts.cartesianSimpleParams(params, orExpression, result) + + val union = result.build() + union.children() shouldHaveSize 4 + } + } + + @Nested internal inner class + `multiply child expressions over an 'OrExpression'` { + + @Test + fun `producing a union of conjunctions`() { + val child = AndExpression.newBuilder() + .addParam(RecordSubjectParameter(is_traded, ComparisonOperator.EQUALS, true)) + .build() + val children = listOf(child) + + val customColumn = object : CustomColumn() { + override fun name(): ColumnName = ColumnName.of("custom") + override fun type(): Class = String::class.java + override fun valueIn(source: Manufacturer): String = "value" + } + val customParam = + CustomSubjectParameter(customColumn, "value", ComparisonOperator.EQUALS) + + val orExpression = OrExpression.newBuilder() + .addParam(RecordSubjectParameter(stock_count, ComparisonOperator.EQUALS, 10)) + .addParam(RecordSubjectParameter(stock_count, ComparisonOperator.EQUALS, 20)) + .addCustomParam(customParam) + .build() + + val result = OrExpression.newBuilder() + CartesianProducts.cartesianChildren(children, orExpression, result) + + val union = result.build() + union.children() shouldHaveSize 3 + union.children().forEach { + it.operator() shouldBe LogicalOperator.AND + } + } + + @Test + fun `producing a union of conjunctions when child is an 'OrExpression'`() { + val innerOr = OrExpression.newBuilder() + .addParam(RecordSubjectParameter(is_traded, ComparisonOperator.EQUALS, true)) + .build() + val children = listOf(innerOr) + + val orExpression = OrExpression.newBuilder() + .addParam(RecordSubjectParameter(stock_count, ComparisonOperator.EQUALS, 10)) + .build() + + val result = OrExpression.newBuilder() + CartesianProducts.cartesianChildren(children, orExpression, result) + + val union = result.build() + union.children() shouldHaveSize 1 + val and = union.children()[0] as AndExpression + and.children() shouldHaveSize 1 + and.children()[0] shouldBe innerOr + } + } + + @Nested internal inner class + `multiply custom parameters over an 'OrExpression'` { + + @Test + fun `producing a union of conjunctions`() { + val column = object : CustomColumn() { + override fun name(): ColumnName = ColumnName.of("custom") + override fun type(): Class = String::class.java + override fun valueIn(source: Manufacturer): String = "value" + } + val param = CustomSubjectParameter(column, "value", ComparisonOperator.EQUALS) + val params = listOf(param) + + val orExpression = OrExpression.newBuilder() + .addParam(RecordSubjectParameter(stock_count, ComparisonOperator.EQUALS, 10)) + .addCustomParam(param) + .addExpression(AndExpression.newBuilder() + .addParam(RecordSubjectParameter(is_traded, ComparisonOperator.EQUALS, true)) + .build()) + .build() + + val result = OrExpression.newBuilder() + CartesianProducts.cartesianCustomParams(params, orExpression, result) + + val union = result.build() + union.children() shouldHaveSize 3 + } + } + + @Test + fun `have private constructor`() { + val constructor = CartesianProducts::class.java.getDeclaredConstructor() + constructor.isAccessible = true + val instance = constructor.newInstance() + instance shouldNotBe null + } +} diff --git a/base/src/test/kotlin/io/spine/query/EntityQuerySpec.kt b/base/src/test/kotlin/io/spine/query/EntityQuerySpec.kt new file mode 100644 index 0000000000..bdad4dec68 --- /dev/null +++ b/base/src/test/kotlin/io/spine/query/EntityQuerySpec.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2025, 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.query + +import com.google.common.testing.EqualsTester +import com.google.protobuf.Descriptors.Descriptor +import com.google.protobuf.Empty +import com.google.protobuf.FieldMask +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.spine.base.EntityState +import io.spine.testing.StubMessage +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`EntityQuery` should") +internal class EntityQuerySpec { + + private val secondsColumn = EntityColumn( + "seconds", Long::class.java, { 42L } + ) + + @Test + fun `be converted back to builder`() { + val builder = TestEntityQueryBuilder() + val query = builder.build() + query.toBuilder() shouldBe builder + } + + @Test + fun `be converted to 'RecordQuery'`() { + val builder = TestEntityQueryBuilder() + val criterion = EntityCriterion(secondsColumn, builder) + criterion.`is`(100L) + builder.withMask("seconds") + builder.sortAscendingBy(secondsColumn) + builder.limit(5) + + val query = builder.build() + val recordQuery = query.toRecordQuery() + + recordQuery shouldNotBe null + recordQuery.limit() shouldBe 5 + recordQuery.mask() shouldBe FieldMask.newBuilder().addPaths("seconds").build() + } + + @Test + fun `copy its state to another builder`() { + val builder = TestEntityQueryBuilder() + builder.sortAscendingBy(secondsColumn) + builder.limit(5) + val query = builder.build() + + val anotherBuilder = TestEntityQueryBuilder() + query.copyTo(anotherBuilder) + + val anotherQuery = anotherBuilder.build() + anotherQuery.limit() shouldBe 5 + } + + @Test + fun `support 'equals()' and 'hashCode()'`() { + val builder1 = TestEntityQueryBuilder() + val query1a = builder1.build() + val query1b = builder1.build() + + val builder2 = TestEntityQueryBuilder() + builder2.limit(10) + builder2.sortAscendingBy(secondsColumn) + val query2 = builder2.build() + + EqualsTester() + .addEqualityGroup(query1a, query1b) + .addEqualityGroup(query2) + .testEquals() + } +} + +/** + * A stub entity state for testing purposes. + */ +internal class StubState : StubMessage(), EntityState { + override fun getDescriptorForType(): Descriptor = com.google.protobuf.Timestamp.getDescriptor() + override fun getDefaultInstanceForType(): StubState = INSTANCE + companion object { + private val INSTANCE = StubState() + @JvmStatic + fun getDefaultInstance(): StubState = INSTANCE + } +} + +/** + * A concrete implementation of [EntityQuery] for testing purposes. + */ +private class TestEntityQuery(builder: TestEntityQueryBuilder) : + EntityQuery(builder) + +/** + * A concrete implementation of [EntityQueryBuilder] for testing purposes. + */ +private class TestEntityQueryBuilder : + EntityQueryBuilder( + String::class.java, StubState::class.java + ) { + override fun thisRef(): TestEntityQueryBuilder = this + override fun build(): TestEntityQuery = TestEntityQuery(this) +} diff --git a/base/src/test/kotlin/io/spine/security/InvocationGuardTest.kt b/base/src/test/kotlin/io/spine/security/InvocationGuardTest.kt index d122be6edd..17dbc7115a 100644 --- a/base/src/test/kotlin/io/spine/security/InvocationGuardTest.kt +++ b/base/src/test/kotlin/io/spine/security/InvocationGuardTest.kt @@ -36,9 +36,8 @@ import org.junit.jupiter.api.function.Executable @DisplayName("`InvocationGuard` should") internal class InvocationGuardTest { - @Nested - @DisplayName("throw `SecurityException`") - internal inner class Throwing { + @Nested internal inner class + `throw 'SecurityException'` { @Test fun `if no classes are allowed`() { diff --git a/base/src/test/kotlin/io/spine/string/FnStringifierSpec.kt b/base/src/test/kotlin/io/spine/string/FnStringifierSpec.kt new file mode 100644 index 0000000000..d488f5645e --- /dev/null +++ b/base/src/test/kotlin/io/spine/string/FnStringifierSpec.kt @@ -0,0 +1,49 @@ +/* + * 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.string + +import io.kotest.matchers.shouldBe +import io.spine.util.SerializableFunction +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`FnStringifier` should") +class FnStringifierSpec { + + private class StubStringifier : FnStringifier( + "stub", + SerializableFunction { it.toString() }, + SerializableFunction { it.toInt() } + ) + + @Test + fun `convert using functions`() { + val stringifier = StubStringifier() + stringifier.convert(10) shouldBe "10" + stringifier.reverse().convert("20") shouldBe 20 + } +} diff --git a/base/src/test/kotlin/io/spine/string/StringifiersSpec.kt b/base/src/test/kotlin/io/spine/string/StringifiersSpec.kt index 81b43cd8be..1aeb58dae8 100644 --- a/base/src/test/kotlin/io/spine/string/StringifiersSpec.kt +++ b/base/src/test/kotlin/io/spine/string/StringifiersSpec.kt @@ -39,6 +39,8 @@ import io.spine.base.Time import io.spine.string.Stringifiers.stringify import io.spine.test.string.STask import io.spine.test.string.STaskId +import io.spine.test.string.sTask +import io.spine.test.string.sTaskId import io.spine.test.string.STaskStatus import io.spine.testing.UtilityClassTest import io.spine.type.toCompactJson @@ -57,9 +59,8 @@ internal class StringifiersSpec : UtilityClassTest(Stringifiers::c private const val SIZE = 5 } - @Nested - @DisplayName("stringify") - internal inner class StringifyKt { + @Nested internal inner class + `stringify` { @Test fun boolean() = checkStringifies(false, "false") @@ -97,13 +98,13 @@ internal class StringifiersSpec : UtilityClassTest(Stringifiers::c @Test fun `a Protobuf 'Message'`() { - val id = STaskId.newBuilder() - .setUuid(Identifier.newUuid()) - .build() - val message = STask.newBuilder() - .setId(id) - .setStatus(STaskStatus.DONE) - .build() + val id = sTaskId { + uuid = Identifier.newUuid() + } + val message = sTask { + this.id = id + status = STaskStatus.DONE + } val expected = message.toCompactJson() checkStringifies(message, expected) @@ -114,10 +115,8 @@ internal class StringifiersSpec : UtilityClassTest(Stringifiers::c } } - @Nested - @DisplayName("create 'Stringifier' with a delimeter for") - internal inner class Delimited { - + @Nested internal inner class + `create 'Stringifier' with a delimiter for` { @Test fun List() { val stamps: List = createList() @@ -169,9 +168,8 @@ internal class StringifiersSpec : UtilityClassTest(Stringifiers::c * This class covers only cases that are not touched by other tests that * involve parsing of string values. */ - @Nested - @DisplayName("parse a string into") - internal inner class Parsing { + @Nested internal inner class + `parse a string into` { @Test fun Boolean() { diff --git a/base/src/test/kotlin/io/spine/string/StringifySpec.kt b/base/src/test/kotlin/io/spine/string/StringifySpec.kt new file mode 100644 index 0000000000..6529f44ae5 --- /dev/null +++ b/base/src/test/kotlin/io/spine/string/StringifySpec.kt @@ -0,0 +1,66 @@ +/* + * 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.string + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("Top-level functions in `Stringify.kt` should") +class StringifySpec { + + @Test + fun `stringify an object`() { + val value = 123 + value.stringify() shouldBe "123" + } + + @Test + fun `parse from string using type parameter`() { + fromString("123") shouldBe 123 + } + + @Test + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "RemoveRedundantQualifierName") + // To use `java.lang.Integer` instead of `kotlin.Int` for testing. + fun `parse from string using 'KClass'`() { + fromString("123", java.lang.Integer::class) shouldBe 123 + } + + @Test + fun `create list stringifier`() { + listStringifier().convert(listOf(1, 2)) shouldBe "\"1\",\"2\"" + listStringifier('#').convert(listOf(1, 2)) shouldBe "\"1\"#\"2\"" + } + + @Test + fun `create map stringifier`() { + mapStringifier().convert(mapOf(1 to "a")) shouldBe "\"1\":\"a\"" + mapStringifier('#').convert(mapOf(1 to "a", 2 to "b")) shouldBe + "\"1\":\"a\"#\"2\":\"b\"" + } +} diff --git a/base/src/test/kotlin/io/spine/type/ApiOptionSpec.kt b/base/src/test/kotlin/io/spine/type/ApiOptionSpec.kt index 81dee8339a..a07431627f 100644 --- a/base/src/test/kotlin/io/spine/type/ApiOptionSpec.kt +++ b/base/src/test/kotlin/io/spine/type/ApiOptionSpec.kt @@ -51,9 +51,8 @@ import org.junit.jupiter.api.Test @DisplayName("`ApiOption` should") class ApiOptionSpec { - @Nested - @DisplayName("provide items for API stability annotations") - inner class ApiStabilityEnum { + @Nested inner class + `provide items for API stability annotations` { @Test fun beta() { @@ -85,9 +84,8 @@ class ApiOptionSpec { } } - @Nested - @DisplayName("locate itself in descriptors") - inner class LocatingInDescriptor { + @Nested inner class + `locate itself in descriptors` { @Test fun beta() { diff --git a/base/src/test/kotlin/io/spine/type/DescriptorExtsSpec.kt b/base/src/test/kotlin/io/spine/type/DescriptorExtsSpec.kt index ee4b043673..78c5f6a28e 100644 --- a/base/src/test/kotlin/io/spine/type/DescriptorExtsSpec.kt +++ b/base/src/test/kotlin/io/spine/type/DescriptorExtsSpec.kt @@ -46,9 +46,8 @@ import org.junit.jupiter.api.Test @DisplayName("`Descriptor` extensions from `io.spine.type` should") internal class DescriptorExtsSpec { - @Nested - @DisplayName("tell that a message type is marked as") - inner class ExplicitOption { + @Nested inner class + `tell that a message type is marked as` { @Test fun beta() { diff --git a/base/src/test/kotlin/io/spine/type/FileDescriptorExtsSpec.kt b/base/src/test/kotlin/io/spine/type/FileDescriptorExtsSpec.kt index 2509470823..92416bae11 100644 --- a/base/src/test/kotlin/io/spine/type/FileDescriptorExtsSpec.kt +++ b/base/src/test/kotlin/io/spine/type/FileDescriptorExtsSpec.kt @@ -46,9 +46,8 @@ import org.junit.jupiter.api.Test @DisplayName("`FileDescriptor` extensions from `io.spine.type` should") internal class FileDescriptorExtsSpec { - @Nested - @DisplayName("tell if all types in a file are") - inner class AllTypesAre { + @Nested inner class + `tell if all types in a file are` { @Test fun beta() { diff --git a/base/src/test/kotlin/io/spine/type/JsonSpec.kt b/base/src/test/kotlin/io/spine/type/JsonSpec.kt index fd87c115c3..8b02abd7ed 100644 --- a/base/src/test/kotlin/io/spine/type/JsonSpec.kt +++ b/base/src/test/kotlin/io/spine/type/JsonSpec.kt @@ -31,6 +31,7 @@ import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.spine.base.Identifier import io.spine.json.given.Node +import io.spine.json.given.node import io.spine.json.given.WrappedString import io.spine.testing.Assertions.assertNpe import io.spine.testing.TestValues @@ -55,10 +56,10 @@ internal class JsonSpec { @Test fun `print to compact JSON`() { val idValue = Identifier.newUuid() - val node = Node.newBuilder() - .setName(idValue) - .setRight(Node.getDefaultInstance()) - .build() + val node = node { + name = idValue + right = Node.getDefaultInstance() + } val result = node.toCompactJson() result shouldNotBe "" diff --git a/base/src/test/kotlin/io/spine/type/KnownTypesSpec.kt b/base/src/test/kotlin/io/spine/type/KnownTypesSpec.kt index cb1760f0fb..3fdd08cd42 100644 --- a/base/src/test/kotlin/io/spine/type/KnownTypesSpec.kt +++ b/base/src/test/kotlin/io/spine/type/KnownTypesSpec.kt @@ -106,9 +106,8 @@ internal class KnownTypesSpec { found.isEmpty() shouldBe false } - @Nested - @DisplayName("contain types") - internal inner class ContainTypes { + @Nested internal inner class + `contain types` { @Test fun `defined by Spine framework`() { @@ -181,8 +180,7 @@ internal class KnownTypesSpec { } @Test - @DisplayName("throw UnknownTypeException for requesting info on an unknown type") - fun throwOnUnknownType() { + fun `throw 'UnknownTypeException' for requesting info on an unknown type`() { val unexpectedUrl = TypeUrl.parse("prefix/unexpected.type") assertUnknownType { knownTypes.classNameOf(unexpectedUrl) } } diff --git a/base/src/test/kotlin/io/spine/type/MessageExtsSpec.kt b/base/src/test/kotlin/io/spine/type/MessageExtsSpec.kt index aa2ddb9a06..e9027e5a6e 100644 --- a/base/src/test/kotlin/io/spine/type/MessageExtsSpec.kt +++ b/base/src/test/kotlin/io/spine/type/MessageExtsSpec.kt @@ -45,9 +45,8 @@ internal class MessageExtsSpec { "spine.given.type.ExplicitInternalType" } - @Nested - @DisplayName("tell if a message is internal") - inner class InternalMessage { + @Nested inner class + `tell if a message is internal` { @Test fun `by class annotation`() { diff --git a/base/src/test/kotlin/io/spine/type/ProtoTextExtsSpec.kt b/base/src/test/kotlin/io/spine/type/ProtoTextExtsSpec.kt index 613d1455d7..ef124c8071 100644 --- a/base/src/test/kotlin/io/spine/type/ProtoTextExtsSpec.kt +++ b/base/src/test/kotlin/io/spine/type/ProtoTextExtsSpec.kt @@ -48,9 +48,8 @@ import org.junit.jupiter.api.Test @DisplayName("Kotlin extensions for proto text output should") internal class ProtoTextExtsSpec { - @Nested - @DisplayName("print short debug string of") - inner class DebugStrOutput { + @Nested inner class + `print short debug string of` { private lateinit var msg: Timestamp private lateinit var debugStr: String @@ -88,9 +87,8 @@ internal class ProtoTextExtsSpec { } } - @Nested - @DisplayName("print proto text output with name which") - inner class TextOutput { + @Nested inner class + `print proto text output with name which` { private lateinit var msg: MapOfAnys private lateinit var textOut: String diff --git a/base/src/test/kotlin/io/spine/type/ServiceDescriptorExtsSpec.kt b/base/src/test/kotlin/io/spine/type/ServiceDescriptorExtsSpec.kt index 91c18dff91..a50936ead0 100644 --- a/base/src/test/kotlin/io/spine/type/ServiceDescriptorExtsSpec.kt +++ b/base/src/test/kotlin/io/spine/type/ServiceDescriptorExtsSpec.kt @@ -37,9 +37,8 @@ import org.junit.jupiter.api.Test @DisplayName("`ServiceDescriptor` extensions in `io.spine.type` should") internal class ServiceDescriptorExtsSpec { - @Nested - @DisplayName("tell if a service is") - inner class SpiAnnotations { + @Nested inner class + `tell if a service is` { @Test fun `explicitly annotated SPI`() { diff --git a/base/src/test/kotlin/io/spine/type/TypeUrlSpec.kt b/base/src/test/kotlin/io/spine/type/TypeUrlSpec.kt index 2436a20604..fa7d25a54d 100644 --- a/base/src/test/kotlin/io/spine/type/TypeUrlSpec.kt +++ b/base/src/test/kotlin/io/spine/type/TypeUrlSpec.kt @@ -29,6 +29,7 @@ package io.spine.type import com.google.common.testing.EqualsTester import com.google.common.testing.SerializableTester import com.google.protobuf.Any +import com.google.protobuf.any import com.google.protobuf.AnyProto import com.google.protobuf.BoolValue import com.google.protobuf.Descriptors.Descriptor @@ -77,7 +78,7 @@ internal class TypeUrlSpec { `create an instance by` { @Test - fun message() { + fun `a message`() { val msg = TypeConverter.toMessage(Identifier.newUuid()) val typeUrl = TypeUrl.of(msg) assertTypeUrl(typeUrl) @@ -188,8 +189,7 @@ internal class TypeUrlSpec { } @Test - @DisplayName("created for a type declared in a file with empty `(type_url_prefix)`") - fun inFile() { + fun `created for a type declared in a file with empty '(type_url_prefix)'`() { noPrefixType = TypeUrl.from(TypeWithoutPrefix.getDescriptor()) assertEmptyPrefix() } @@ -214,9 +214,9 @@ internal class TypeUrlSpec { @Test fun `invalid URL of a packed message`() { - val any = Any.newBuilder() - .setTypeUrl("invalid_type_url") - .build() + val any = any { + typeUrl = "invalid_type_url" + } val exception = assertThrows { TypeUrl.ofEnclosed(any) } @@ -319,7 +319,7 @@ internal class TypeUrlSpec { } @Test - fun serialize() { + fun `be serializable`() { SerializableTester.reserializeAndAssert(TypeUrl.of(Timestamp::class.java)) } } diff --git a/base/src/test/kotlin/io/spine/util/Math2Spec.kt b/base/src/test/kotlin/io/spine/util/Math2Spec.kt new file mode 100644 index 0000000000..6066aaeb6d --- /dev/null +++ b/base/src/test/kotlin/io/spine/util/Math2Spec.kt @@ -0,0 +1,68 @@ +/* + * 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.util + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.spine.testing.UtilityClassTest +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`Math2` should") +class Math2Spec : UtilityClassTest(Math2::class.java) { + + @Test + fun `multiply long by int`() { + Math2.safeMultiply(10L, 2) shouldBe 20L + Math2.safeMultiply(10L, 0) shouldBe 0L + Math2.safeMultiply(10L, 1) shouldBe 10L + Math2.safeMultiply(10L, -1) shouldBe -10L + Math2.safeMultiply(Long.MAX_VALUE, 1) shouldBe Long.MAX_VALUE + } + + @Test + fun `fail to multiply on overflow`() { + shouldThrow { + Math2.safeMultiply(Long.MAX_VALUE, 2) + } + shouldThrow { + Math2.safeMultiply(Long.MIN_VALUE, -1) + } + } + + @Test + fun `perform floor division`() { + Math2.floorDiv(0, 4) shouldBe 0L + Math2.floorDiv(-1, 4) shouldBe -1L + Math2.floorDiv(-2, 4) shouldBe -1L + Math2.floorDiv(-3, 4) shouldBe -1L + Math2.floorDiv(-4, 4) shouldBe -1L + Math2.floorDiv(-5, 4) shouldBe -2L + Math2.floorDiv(4, 4) shouldBe 1L + Math2.floorDiv(5, 4) shouldBe 1L + } +} diff --git a/base/src/test/kotlin/io/spine/util/Preconditions2Test.kt b/base/src/test/kotlin/io/spine/util/Preconditions2Test.kt index 18488dd0be..788092d0f9 100644 --- a/base/src/test/kotlin/io/spine/util/Preconditions2Test.kt +++ b/base/src/test/kotlin/io/spine/util/Preconditions2Test.kt @@ -86,8 +86,8 @@ internal class Preconditions2Test : UtilityClassTest(Preconditio } } - @Nested - internal inner class `check that a 'String' is` : + @Nested internal inner class + `check that a 'String' is` : TestSuite(Preconditions2::checkNotEmptyOrBlank, { arg, fmt, params -> checkNotEmptyOrBlank(arg, fmt, params) }) { @@ -129,8 +129,8 @@ internal class Preconditions2Test : UtilityClassTest(Preconditio } - @Nested - internal inner class `check that a value is positive` : + @Nested internal inner class + `check that a value is positive` : TestSuite(Preconditions2::checkPositive,{ arg, fmt, params -> checkPositive(arg, fmt, params) }) { @@ -154,8 +154,8 @@ internal class Preconditions2Test : UtilityClassTest(Preconditio } } - @Nested - internal inner class `check that a value is positive or zero` : + @Nested internal inner class + `check that a value is positive or zero` : TestSuite(Preconditions2::checkNonNegative, { arg, fmt, params -> checkNonNegative(arg, fmt, params) }) { @@ -183,9 +183,8 @@ internal class Preconditions2Test : UtilityClassTest(Preconditio assertIllegalArgument { checkBounds(10, "checked value", -5, 9) } } - @Nested - @DisplayName("check that a message is not in the default state") - internal inner class NotDefaultMessage { + @Nested internal inner class + `check that a message is not in the default state` { private val defaultValue: Message = StringValue.getDefaultInstance() private var customErrorMessage: String? = null diff --git a/base/src/test/kotlin/io/spine/util/TextTest.kt b/base/src/test/kotlin/io/spine/util/TextTest.kt deleted file mode 100644 index 0d0ba9a404..0000000000 --- a/base/src/test/kotlin/io/spine/util/TextTest.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2022, 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 - * - * http://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.util - -import com.google.common.testing.NullPointerTester -import com.google.common.truth.Truth.assertThat -import io.kotest.matchers.shouldBe -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows - -@Suppress("DEPRECATION") -internal class TextTest { - - private val nl = System.lineSeparator() - - @Test - fun `handle nulls passed to static methods`() { - NullPointerTester().testAllPublicStaticMethods(Text::class.java) - } - - @Test - fun `split text into lines`() { - val str = "uno${nl}dos${nl}tres" - val text = Text(str) - assertThat(text.lines()).containsExactly("uno", "dos", "tres") - } - - @Test - fun `join 'Iterable'`() { - val iterable = listOf("bir", "iki", "รผรง") - val text = Text(iterable) - text.toString() shouldBe "bir${nl}iki${nl}รผรง" - } - - @Test - fun `join an array`() { - val array = arrayOf("one", "two", "three") - val text = Text(array) - text.toString() shouldBe "one${nl}two${nl}three" - } - - @Test - fun `find substring`() { - val text = Text.of("abra", "ka", "dabra") - - assertThrows { text.contains("abra${nl}ka") } - - text.contains("abra") shouldBe true - text.contains("kada") shouldBe false - } - - @Test - fun `must not accept lines with separators`() { - assertThrows { Text.of("un", "${nl}o") } - assertThrows { Text(listOf("dos", "tres${nl}")) } - } - - @Test - fun `always return the same value`() { - val text = Text.of("donna", "be", "la", "mare") - - val value = text.value() - assertThat(value).isSameInstanceAs(text.value()) - } -} diff --git a/build.gradle.kts b/build.gradle.kts index f194f9d811..170a768375 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,11 +1,11 @@ /* - * Copyright 2023, 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. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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 @@ -32,7 +32,7 @@ import io.spine.dependency.local.Logging import io.spine.gradle.publish.PublishingRepos import io.spine.gradle.publish.spinePublishing import io.spine.gradle.repo.standardToSpineSdk -import io.spine.gradle.report.coverage.JacocoConfig +import io.spine.gradle.report.coverage.KoverConfig import io.spine.gradle.report.license.LicenseReporter import io.spine.gradle.report.pom.PomGenerator @@ -46,7 +46,6 @@ buildscript { plugins { kotlin - jacoco `gradle-doctor` `project-report` `dokka-setup` @@ -113,8 +112,9 @@ val dokkaGeneratePublicationHtml by tasks.getting { dependsOn(tasks.jar) } +KoverConfig.applyTo(project) + gradle.projectsEvaluated { - JacocoConfig.applyTo(project) LicenseReporter.mergeAllReports(project) PomGenerator.applyTo(project) } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 56f3530450..23abc30bf5 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -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. @@ -132,7 +132,7 @@ val kotestJvmPluginVersion = "0.4.10" /** * @see [io.spine.dependency.test.Kover] */ -val koverVersion = "0.9.1" +val koverVersion = "0.9.8" /** * The version of the Shadow Plugin. diff --git a/buildSrc/src/main/kotlin/DokkaExts.kt b/buildSrc/src/main/kotlin/DokkaExts.kt index 3c72e3bc91..800b3236cb 100644 --- a/buildSrc/src/main/kotlin/DokkaExts.kt +++ b/buildSrc/src/main/kotlin/DokkaExts.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. @@ -25,6 +25,7 @@ */ import io.spine.dependency.build.Dokka +import io.spine.gradle.SpineTaskGroup import io.spine.gradle.publish.getOrCreate import java.io.File import java.time.LocalDate @@ -204,6 +205,8 @@ fun TaskContainer.dokkaJavadocTask(): Task? = this.findByName("dokkaGeneratePubl * applying `dokka-setup` plugin. */ fun Project.htmlDocsJar(): TaskProvider = tasks.getOrCreate("htmlDocsJar") { + group = SpineTaskGroup.name + description = "Assembles a JAR with generated Dokka HTML docs" archiveClassifier.set("html-docs") from(files(dokkaHtmlOutput())) diff --git a/buildSrc/src/main/kotlin/config-tester.gradle.kts b/buildSrc/src/main/kotlin/config-tester.gradle.kts index 21d31e3630..7b64dac7d1 100644 --- a/buildSrc/src/main/kotlin/config-tester.gradle.kts +++ b/buildSrc/src/main/kotlin/config-tester.gradle.kts @@ -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. @@ -26,6 +26,7 @@ import io.spine.gradle.ConfigTester import io.spine.gradle.SpineRepos +import io.spine.gradle.SpineTaskGroup import io.spine.gradle.cleanFolder import java.nio.file.Path import java.nio.file.Paths @@ -51,6 +52,8 @@ ConfigTester(config, tasks, tempFolder) // Cleans the temp folder used to check out the sources from Git. tasks.register("clean") { + group = SpineTaskGroup.name + description = "Removes the temp folder used by `ConfigTester` to check out external sources" doLast { cleanFolder(tempFolder) } diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/test/Kover.kt b/buildSrc/src/main/kotlin/io/spine/dependency/test/Kover.kt index 61897cc85e..93ef593b4d 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/test/Kover.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/test/Kover.kt @@ -29,7 +29,7 @@ package io.spine.dependency.test // https://github.com/Kotlin/kotlinx-kover @Suppress("unused", "ConstPropertyName") object Kover { - const val version = "0.9.1" + const val version = "0.9.8" const val id = "org.jetbrains.kotlinx.kover" const val classpath = "org.jetbrains.kotlinx:kover-gradle-plugin:$version" } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.kt b/buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.kt index e5c3007d2e..c3bbfbe115 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.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. @@ -98,6 +98,8 @@ class ConfigTester( val tasksPerRepo = repos.map { testWithConfig(it) } tasks.register(taskName) { + group = SpineTaskGroup.name + description = "Builds every configured downstream repository against this `config`" for (repoTaskName in tasksPerRepo) { dependsOn(repoTaskName) } @@ -119,6 +121,8 @@ class ConfigTester( runGradleName: String ) { tasks.register(executeBuildName) { + group = SpineTaskGroup.name + description = "Checks out `${gitRepo.name}` and overlays local `config` and `buildSrc`" doLast { println(" *** Testing `config` and `config/buildSrc` with `${gitRepo.name}`. ***") val ignoredFolder = tempFolder.toPath() @@ -134,6 +138,8 @@ class ConfigTester( gitRepo: GitRepository, ) { tasks.register(runGradleName, RunBuild::class.java) { + group = SpineTaskGroup.name + description = "Runs the Gradle build of `${gitRepo.name}` against the local `config`" doFirst { println("`${gitRepo.name}`: starting Gradle build...") } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/SpineTaskGroup.kt b/buildSrc/src/main/kotlin/io/spine/gradle/SpineTaskGroup.kt new file mode 100644 index 0000000000..073fe5d3eb --- /dev/null +++ b/buildSrc/src/main/kotlin/io/spine/gradle/SpineTaskGroup.kt @@ -0,0 +1,49 @@ +/* + * 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 + +/** + * The Gradle task group used by every custom task registered or + * configured by Spine SDK code. + * + * Setting `group = SpineTaskGroup.name` on every Spine-specific task + * keeps them listed together under `spine` in `./gradlew tasks` and + * in the IntelliJ IDEA Gradle tool window. See + * `.agents/skills/gradle-review/spine-task-conventions.md` in the + * `config` repository for the full convention and rationale. + * + * Example: + * ``` + * tasks.register("generateSpineModel") { + * group = SpineTaskGroup.name + * description = "Generates Spine model classes from .proto definitions" + * } + * ``` + */ +object SpineTaskGroup { + const val name = "spine" +} diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/Build.kt b/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/Build.kt index 163747e8a5..3f1ca120c7 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/Build.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/Build.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. @@ -26,6 +26,7 @@ package io.spine.gradle.dart.task +import io.spine.gradle.SpineTaskGroup import io.spine.gradle.TaskName import io.spine.gradle.base.assemble import io.spine.gradle.base.check @@ -98,7 +99,7 @@ private fun DartTasks.resolveDependencies(): TaskProvider = register(resolveDependenciesName) { description = "Fetches dependencies declared via `pubspec.yaml`." - group = DartTasks.Group.build + group = SpineTaskGroup.name mustRunAfter(cleanPackageIndex) @@ -125,7 +126,7 @@ private fun DartTasks.cleanPackageIndex(): TaskProvider = register(cleanPackageIndexName) { description = "Deletes the resolved `.packages` and `package_config.json` files." - group = DartTasks.Group.build + group = SpineTaskGroup.name delete( packageIndex, @@ -147,7 +148,7 @@ private fun DartTasks.testDart(): TaskProvider = register(testDartName) { description = "Runs Dart tests declared in the `./test` directory." - group = DartTasks.Group.build + group = SpineTaskGroup.name dependsOn(resolveDependencies) diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/DartTasks.kt b/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/DartTasks.kt index bc5e1e93ef..65f94e2307 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/DartTasks.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/DartTasks.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. @@ -38,7 +38,6 @@ import org.gradle.api.tasks.TaskContainer * * 1. Access to the current [DartContext]. * 2. Project's [TaskContainer]. - * 3. Default task groups. * * Supposing, one needs to create a new task that would participate in building. Let the task name * be `testDart`. To do that, several steps should be completed: @@ -53,6 +52,7 @@ import org.gradle.api.tasks.TaskContainer * Here's an example of `testDart()` extension: * * ``` + * import io.spine.gradle.SpineTaskGroup * import io.spine.gradle.named * import io.spine.gradle.register * import io.spine.gradle.TaskName @@ -75,8 +75,8 @@ import org.gradle.api.tasks.TaskContainer * fun DartTasks.testDart() = * register(testDartName) { * - * description = "Runs Dart tests declared in the `./test` directory." - * group = DartTasks.Group.build + * description = "Runs Dart tests declared in the `./test` directory" + * group = SpineTaskGroup.name * * // ... * } @@ -102,14 +102,3 @@ import org.gradle.api.tasks.TaskContainer */ class DartTasks(dartEnv: DartEnvironment, project: Project) : DartContext(dartEnv, project), TaskContainer by project.tasks -{ - /** - * Default task groups for tasks that participate in building a Dart module. - * - * @see [org.gradle.api.Task.getGroup] - */ - internal object Group { - const val build = "Dart/Build" - const val publish = "Dart/Publish" - } -} diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/IntegrationTest.kt b/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/IntegrationTest.kt index 19f1f14716..997bcef531 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/IntegrationTest.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/IntegrationTest.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. @@ -26,6 +26,7 @@ package io.spine.gradle.dart.task +import io.spine.gradle.SpineTaskGroup import io.spine.gradle.TaskName import io.spine.gradle.named import io.spine.gradle.register @@ -75,6 +76,9 @@ val TaskContainer.integrationTest: TaskProvider fun DartTasks.integrationTest() = register(integrationTestName) { + group = SpineTaskGroup.name + description = "Runs integration tests of `spine-dart` against a sample application" + dependsOn( resolveDependencies, ":test-app:appBeforeIntegrationTest" diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/Publish.kt b/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/Publish.kt index 2f8df6b4c6..c1d72c023a 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/Publish.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/Publish.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. @@ -26,6 +26,7 @@ package io.spine.gradle.dart.task +import io.spine.gradle.SpineTaskGroup import io.spine.gradle.TaskName import io.spine.gradle.base.assemble import io.spine.gradle.named @@ -92,7 +93,7 @@ private fun DartTasks.stagePubPublication(): TaskProvider = register(stagePubPublicationName) { description = "Prepares the Dart package for Pub publication." - group = DartTasks.Group.publish + group = SpineTaskGroup.name dependsOn(assemble) @@ -128,7 +129,7 @@ private fun DartTasks.publishToPub(): TaskProvider = register(publishToPubName) { description = "Publishes the prepared publication to Pub." - group = DartTasks.Group.publish + group = SpineTaskGroup.name dependsOn(stagePubPublication) @@ -160,7 +161,7 @@ private fun DartTasks.activateLocally(): TaskProvider = register(activateLocallyName) { description = "Activates this package locally." - group = DartTasks.Group.publish + group = SpineTaskGroup.name dependsOn(stagePubPublication) diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/UpdateGitHubPages.kt b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/UpdateGitHubPages.kt index 860dfbed1e..9782980ac2 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/UpdateGitHubPages.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/UpdateGitHubPages.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. @@ -28,6 +28,7 @@ package io.spine.gradle.github.pages import dokkaHtmlTask import dokkaJavadocTask +import io.spine.gradle.SpineTaskGroup import io.spine.gradle.fs.LazyTempPath import io.spine.gradle.github.pages.TaskName.copyHtmlDocs import io.spine.gradle.github.pages.TaskName.copyJavadocDocs @@ -119,6 +120,8 @@ class UpdateGitHubPages : Plugin { @Suppress("unused") private fun Project.registerNoOpTask() { tasks.register(updateGitHubPages) { + group = SpineTaskGroup.name + description = "Skips the GitHub Pages update for snapshot project versions" doLast { val project = this@registerNoOpTask println( @@ -147,6 +150,8 @@ class UpdateGitHubPages : Plugin { val inputs = composeJavadocInputs() register(copyJavadocDocs, Copy::class.java) { + group = SpineTaskGroup.name + description = "Copies generated Javadoc into the GitHub Pages staging folder" inputs.forEach { from(it) } into(javadocOutputFolder) } @@ -163,6 +168,8 @@ class UpdateGitHubPages : Plugin { val inputs = composeDokkaInputs() register(copyHtmlDocs, Copy::class.java) { + group = SpineTaskGroup.name + description = "Copies generated Dokka HTML docs into the GitHub Pages staging folder" inputs.forEach { from(it) } into(htmlOutputFolder) } @@ -181,6 +188,8 @@ class UpdateGitHubPages : Plugin { private fun TaskContainer.registerUpdateTask(): TaskProvider { return register(updateGitHubPages) { + group = SpineTaskGroup.name + description = "Publishes the generated documentation to the `gh-pages` branch" doLast { try { updateGhPages(project) diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/javadoc/ExcludeInternalDoclet.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javadoc/ExcludeInternalDoclet.kt index c42c65c030..7493677424 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/javadoc/ExcludeInternalDoclet.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/javadoc/ExcludeInternalDoclet.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. @@ -27,6 +27,7 @@ package io.spine.gradle.javadoc import io.spine.dependency.local.ToolBase +import io.spine.gradle.SpineTaskGroup import io.spine.gradle.javadoc.ExcludeInternalDoclet.Companion.taskName import io.spine.gradle.sourceSets import org.gradle.api.Project @@ -93,6 +94,9 @@ private fun Project.appendCustomJavadocTask(excludeInternalDoclet: Configuration val javadocTask = tasks.javadocTask() tasks.register(taskName, Javadoc::class.java) { + group = SpineTaskGroup.name + description = "Generates Javadoc that omits `@Internal` Java APIs" + source = sourceSets.getByName("main").allJava.filter { !it.absolutePath.contains("generated") }.asFileTree diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Assemble.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Assemble.kt index 4b57a4e988..fb0e183eb8 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Assemble.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Assemble.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.javascript.task import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.node.ObjectNode import com.google.protobuf.gradle.GenerateProtoTask +import io.spine.gradle.SpineTaskGroup import io.spine.gradle.TaskName import io.spine.gradle.base.assemble import io.spine.gradle.javascript.plugin.generateJsonParsers @@ -105,7 +106,7 @@ private fun JsTasks.assembleJs() = register(assembleJsName) { description = "Assembles JavaScript sources into consumable artifacts." - group = JsTasks.Group.assemble + group = SpineTaskGroup.name dependsOn( installNodePackages, @@ -130,7 +131,7 @@ private fun JsTasks.compileProtoToJs() = register(compileProtoToJsName) { description = "Compiles Protobuf messages into JavaScript." - group = JsTasks.Group.assemble + group = SpineTaskGroup.name withType() .forEach { dependsOn(it) } @@ -158,7 +159,7 @@ private fun JsTasks.installNodePackages() = register(installNodePackagesName) { description = "Installs module`s Node dependencies." - group = JsTasks.Group.assemble + group = SpineTaskGroup.name inputs.file(packageJson) outputs.dir(nodeModules) @@ -185,7 +186,7 @@ private fun JsTasks.updatePackageVersion() = register(updatePackageVersionName) { description = "Sets a module's version in `package.json`." - group = JsTasks.Group.assemble + group = SpineTaskGroup.name doLast { val objectNode = ObjectMapper() diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Check.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Check.kt index d25c3c2176..9f46d845b4 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Check.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Check.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. @@ -26,6 +26,7 @@ package io.spine.gradle.javascript.task +import io.spine.gradle.SpineTaskGroup import io.spine.gradle.TaskName import io.spine.gradle.base.check import io.spine.gradle.java.test @@ -97,7 +98,7 @@ private fun JsTasks.checkJs() = register(checkJsName) { description = "Runs tests, audits NPM modules and creates a test-coverage report." - group = JsTasks.Group.check + group = SpineTaskGroup.name dependsOn( auditNodePackages, @@ -126,7 +127,7 @@ private fun JsTasks.auditNodePackages() = register(auditNodePackagesName) { description = "Audits the module's Node dependencies." - group = JsTasks.Group.check + group = SpineTaskGroup.name inputs.dir(nodeModules) @@ -161,7 +162,7 @@ private fun JsTasks.coverageJs() = register(coverageJsName) { description = "Runs the JavaScript tests and collects the code coverage." - group = JsTasks.Group.check + group = SpineTaskGroup.name outputs.dir(nycOutput) @@ -186,7 +187,7 @@ private fun JsTasks.testJs() = register(testJsName) { description = "Runs JavaScript tests." - group = JsTasks.Group.check + group = SpineTaskGroup.name doLast { npm("run", "test") diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Clean.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Clean.kt index c5ff835489..6042128fa9 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Clean.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Clean.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. @@ -26,6 +26,7 @@ package io.spine.gradle.javascript.task +import io.spine.gradle.SpineTaskGroup import io.spine.gradle.TaskName import io.spine.gradle.base.clean import io.spine.gradle.named @@ -87,7 +88,7 @@ private fun JsTasks.cleanJs() = register(cleanJsName) { description = "Cleans output of `assembleJs` task and output of its dependants." - group = JsTasks.Group.clean + group = SpineTaskGroup.name delete( assembleJs.map { it.outputs }, @@ -114,7 +115,7 @@ private fun JsTasks.cleanGenerated() = register(cleanGeneratedName) { description = "Cleans generated code and reports." - group = JsTasks.Group.clean + group = SpineTaskGroup.name delete( genProtoMain, diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/IntegrationTest.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/IntegrationTest.kt index 227e33edba..2db59a4f5b 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/IntegrationTest.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/IntegrationTest.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. @@ -26,6 +26,7 @@ package io.spine.gradle.javascript.task +import io.spine.gradle.SpineTaskGroup import io.spine.gradle.TaskName import io.spine.gradle.base.build import io.spine.gradle.named @@ -87,7 +88,7 @@ fun JsTasks.integrationTest() { description = "Runs integration tests of the `spine-web` library " + "against the sample application." - group = JsTasks.Group.check + group = SpineTaskGroup.name dependsOn(build, linkSpineWebModule, ":test-app:appBeforeIntegrationTest") @@ -118,7 +119,7 @@ private fun JsTasks.linkSpineWebModule() = register(linkSpineWebModuleName) { description = "Install unpublished artifact of `spine-web` library as a module dependency." - group = JsTasks.Group.assemble + group = SpineTaskGroup.name dependsOn(":client-js:publishJsLocally") diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/JsTasks.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/JsTasks.kt index 3cf633585e..3b9d814a19 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/JsTasks.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/JsTasks.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. @@ -38,7 +38,6 @@ import org.gradle.api.tasks.TaskContainer * * 1. Access to the current [JsContext]. * 2. Project's [TaskContainer]. - * 3. Default task groups. * * Supposing, one needs to create a new task that would participate in building. Let the task name * be `bundleJs`. To do that, several steps should be completed: @@ -53,6 +52,7 @@ import org.gradle.api.tasks.TaskContainer * Here's an example of `bundleJs()` extension: * * ``` + * import io.spine.gradle.SpineTaskGroup * import io.spine.gradle.named * import io.spine.gradle.register * import io.spine.gradle.TaskName @@ -75,8 +75,8 @@ import org.gradle.api.tasks.TaskContainer * fun JsTasks.bundleJs() = * register(bundleJsName) { * - * description = "Bundles JS sources using `webpack` tool." - * group = JsTasks.Group.build + * description = "Bundles JS sources using `webpack` tool" + * group = SpineTaskGroup.name * * // ... * } @@ -102,17 +102,3 @@ import org.gradle.api.tasks.TaskContainer */ class JsTasks(jsEnv: JsEnvironment, project: Project) : JsContext(jsEnv, project), TaskContainer by project.tasks -{ - /** - * Default task groups for tasks that participate in building a JavaScript module. - * - * @see [org.gradle.api.Task.getGroup] - */ - internal object Group { - const val assemble = "JavaScript/Assemble" - const val check = "JavaScript/Check" - const val clean = "JavaScript/Clean" - const val build = "JavaScript/Build" - const val publish = "JavaScript/Publish" - } -} diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/LicenseReport.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/LicenseReport.kt index c3b3a6ad84..8276bcc064 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/LicenseReport.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/LicenseReport.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. @@ -26,6 +26,7 @@ package io.spine.gradle.javascript.task +import io.spine.gradle.SpineTaskGroup import io.spine.gradle.TaskName import io.spine.gradle.named import io.spine.gradle.register @@ -77,7 +78,7 @@ private fun JsTasks.npmLicenseReport() = register(npmLicenseReportName) { description = "Generates the report on NPM dependencies and their licenses." - group = JsTasks.Group.build + group = SpineTaskGroup.name doLast { diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Publish.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Publish.kt index 7d1baeae14..c126501418 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Publish.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Publish.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. @@ -26,6 +26,7 @@ package io.spine.gradle.javascript.task +import io.spine.gradle.SpineTaskGroup import io.spine.gradle.TaskName import io.spine.gradle.named import io.spine.gradle.publish.publish @@ -90,7 +91,7 @@ private fun JsTasks.transpileSources() = register(transpileSourcesName) { description = "Transpiles JavaScript sources using Babel before their publishing." - group = JsTasks.Group.publish + group = SpineTaskGroup.name doLast { npm("run", "transpile-before-publish") @@ -113,7 +114,7 @@ private fun JsTasks.prepareJsPublication() = register(prepareJsPublicationName) { description = "Prepares the NPM package for publishing." - group = JsTasks.Group.publish + group = SpineTaskGroup.name // We need to copy two files into a destination directory without overwriting its content. // Default `Copy` task is not used since it overwrites the content of a destination @@ -153,7 +154,7 @@ private fun JsTasks.publishJsLocally() = register(publishJsLocallyName) { description = "Publishes the NPM package locally with `npm link`." - group = JsTasks.Group.publish + group = SpineTaskGroup.name doLast { publicationDir.npm("link") @@ -184,7 +185,7 @@ private fun JsTasks.publishJs() = register(publishJsName) { description = "Publishes the NPM package with `npm publish`." - group = JsTasks.Group.publish + group = SpineTaskGroup.name doLast { publicationDir.npm("publish") diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Webpack.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Webpack.kt index 7c82ad7a20..5609c909cf 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Webpack.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Webpack.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. @@ -26,6 +26,7 @@ package io.spine.gradle.javascript.task +import io.spine.gradle.SpineTaskGroup import io.spine.gradle.TaskName import io.spine.gradle.named import io.spine.gradle.register @@ -101,7 +102,7 @@ private fun JsTasks.copyBundledJs() = register(copyBundledJsName) { description = "Copies bundled JavaScript sources to the NPM publication directory." - group = JsTasks.Group.publish + group = SpineTaskGroup.name from(assembleJs.map { it.outputs }) into(webpackPublicationDir) diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt index b6683faf99..1243b04522 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.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. @@ -28,6 +28,7 @@ package io.spine.gradle.publish +import io.spine.gradle.SpineTaskGroup import org.gradle.api.Plugin import org.gradle.api.Project @@ -56,6 +57,8 @@ class IncrementGuard : Plugin { override fun apply(target: Project) { val tasks = target.tasks tasks.register(taskName, CheckVersionIncrement::class.java) { + group = SpineTaskGroup.name + description = "Verifies that the project version was incremented before publishing" repository = CloudArtifactRegistry.repository tasks.getByName("check").dependsOn(this) 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 efe51d60f0..480d3e7ba8 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.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. @@ -27,6 +27,7 @@ package io.spine.gradle.publish import htmlDocsJar +import io.spine.gradle.SpineTaskGroup import io.spine.gradle.isSnapshot import io.spine.gradle.repo.Repository import io.spine.gradle.sourceSets @@ -153,7 +154,10 @@ private fun TaskContainer.getOrCreatePublishTask(): TaskProvider = if (names.contains(PUBLISH_TASK)) { named(PUBLISH_TASK) } else { - register(PUBLISH_TASK) + register(PUBLISH_TASK) { + group = SpineTaskGroup.name + description = "Aggregates `publish` tasks of all subprojects" + } } @Suppress( @@ -165,6 +169,7 @@ private fun TaskContainer.registerCheckCredentialsTask( destinations: Set, ): TaskProvider { val checkCredentials = "checkCredentials" + val taskDescription = "Checks credentials for the configured publishing destinations" try { // The result of this call is ignored intentionally. // @@ -176,10 +181,16 @@ private fun TaskContainer.registerCheckCredentialsTask( // for some previously asked `destinations`. named(checkCredentials) val toConfigure = replace(checkCredentials) + toConfigure.group = SpineTaskGroup.name + toConfigure.description = taskDescription toConfigure.doLastCredentialsCheck(destinations) return named(checkCredentials) } catch (_: Exception) { - return register(checkCredentials) { doLastCredentialsCheck(destinations) } + return register(checkCredentials) { + group = SpineTaskGroup.name + description = taskDescription + doLastCredentialsCheck(destinations) + } } } @@ -233,6 +244,8 @@ fun TaskContainer.excludeGoogleProtoFromArtifacts() { * For Proto sources to be included โ€“ [special treatment][protoSources] is needed. */ fun Project.sourcesJar(): TaskProvider = tasks.getOrCreate("sourcesJar") { + group = SpineTaskGroup.name + description = "Assembles a JAR with Java, Kotlin, and Proto sources from the `main` source set" dependOnGenerateProto() archiveClassifier.set("sources") from(sourceSets["main"].allSource) // Puts Java and Kotlin sources. @@ -247,6 +260,8 @@ fun Project.sourcesJar(): TaskProvider = tasks.getOrCreate("sourcesJar") { * [Proto sources][protoSources] from `main` source set. */ fun Project.protoJar(): TaskProvider = tasks.getOrCreate("protoJar") { + group = SpineTaskGroup.name + description = "Assembles a JAR with Proto sources from the `main` source set" dependOnGenerateProto() archiveClassifier.set("proto") from(protoSources()) @@ -259,6 +274,8 @@ fun Project.protoJar(): TaskProvider = tasks.getOrCreate("protoJar") { * of `test` source set. */ internal fun Project.testJar(): TaskProvider = tasks.getOrCreate("testJar") { + group = SpineTaskGroup.name + description = "Assembles a JAR with compiled output of the `test` source set" archiveClassifier.set("test") from(sourceSets["test"].output) } @@ -271,6 +288,8 @@ internal fun Project.testJar(): TaskProvider = tasks.getOrCreate("testJar") * apply the Dokka plugin. It tunes `javadoc` task to generate docs upon Kotlin sources as well. */ fun Project.javadocJar(): TaskProvider = tasks.getOrCreate("javadocJar") { + group = SpineTaskGroup.name + description = "Assembles a JAR with generated Javadoc" archiveClassifier.set("javadoc") val javadocFiles = layout.buildDirectory.dir("dokka/javadoc") from(javadocFiles) @@ -304,7 +323,6 @@ internal fun Project.artifacts(jarFlags: JarFlags): Set> { tasks.add(javadocJar()) tasks.add(htmlDocsJar()) - // We don't want to have an empty "proto.jar" when a project doesn't have any Proto files. if (hasProto()) { tasks.add(protoJar()) diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/CodebaseFilter.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/CodebaseFilter.kt index b6451d9c2e..8f47847caa 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/CodebaseFilter.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/CodebaseFilter.kt @@ -41,6 +41,14 @@ import org.gradle.api.tasks.SourceSetOutput * Works on top of the passed [source][srcDirs] and [output][outputDirs] directories, by analyzing * the source file names and finding the corresponding compiler output. */ +@Deprecated( + message = "Used only by the deprecated `JacocoConfig` pipeline. " + + "Generated-code filtering moved to `KoverConfig.applyTo(rootProject)`, " + + "which derives the exclusion list at configuration time and pushes " + + "it into both per-module and root Kover reports. " + + "Removed when `JacocoConfig` is.", + level = DeprecationLevel.WARNING +) internal class CodebaseFilter( private val project: Project, private val srcDirs: Set, diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/FileExtension.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/FileExtension.kt index 6b97c7baa1..7dad4345dd 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/FileExtension.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/FileExtension.kt @@ -29,6 +29,14 @@ package io.spine.gradle.report.coverage /** * File extensions. */ +@Deprecated( + message = "Used only by the deprecated `JacocoConfig` pipeline. " + + "Removed when `JacocoConfig` is. " + + "See `KoverConfig` for the Kover-based successor and " + + "`.agents/skills/raise-coverage/references/migrate-to-kover.md` " + + "for the migration recipe.", + level = DeprecationLevel.WARNING +) internal enum class FileExtension(val value: String) { /** diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/FileExtensions.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/FileExtensions.kt index 0693562132..c32f682928 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/FileExtensions.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/FileExtensions.kt @@ -20,7 +20,7 @@ * 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 TE USE + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ @@ -60,6 +60,15 @@ private const val KOTLIN_FILE_CLASS_SUFFIX = "Kt" * If the absolute path of this file has either no [precedingMarker] or no [extension], * returns `null`. */ +@Deprecated( + message = "Used only by the deprecated `JacocoConfig` pipeline. " + + "Removed when `JacocoConfig` is. " + + "See `KoverConfig` for the Kover-based successor and " + + "`.agents/skills/raise-coverage/references/migrate-to-kover.md` " + + "for the migration recipe.", + level = DeprecationLevel.WARNING +) +@Suppress("DEPRECATION") internal fun File.parseClassName( precedingMarker: PathMarker, extension: FileExtension @@ -85,6 +94,15 @@ internal fun File.parseClassName( * If the `.class` file corresponds to the anonymous or nested class, only the name of the * top-level enclosing class is returned. */ +@Deprecated( + message = "Used only by the deprecated `JacocoConfig` pipeline. " + + "Removed when `JacocoConfig` is. " + + "See `KoverConfig` for the Kover-based successor and " + + "`.agents/skills/raise-coverage/references/migrate-to-kover.md` " + + "for the migration recipe.", + level = DeprecationLevel.WARNING +) +@Suppress("DEPRECATION") internal fun File.asJavaCompiledClassName(): String? { var className = this.parseClassName(MAIN_OUTPUT_FOLDER, COMPILED_CLASS) if (className != null && className.contains(ANONYMOUS_CLASS.infix)) { @@ -111,6 +129,14 @@ internal fun File.asJavaCompiledClassName(): String? { * * Returns an empty list if this file is not located under [sourceRoot]. */ +@Deprecated( + message = "Used only by the deprecated `JacocoConfig` pipeline. " + + "Removed when `JacocoConfig` is. " + + "See `KoverConfig` for the Kover-based successor and " + + "`.agents/skills/raise-coverage/references/migrate-to-kover.md` " + + "for the migration recipe.", + level = DeprecationLevel.WARNING +) internal fun File.classNamesIn(sourceRoot: File): List { if (!this.startsWith(sourceRoot)) { return emptyList() @@ -136,5 +162,14 @@ private fun String.toFqn(): String = this.replace(File.separatorChar, '.') /** * Tells whether this file is a part of the generated sources, and not produced by a human. */ +@Deprecated( + message = "Used only by the deprecated `JacocoConfig` pipeline. " + + "Removed when `JacocoConfig` is. " + + "See `KoverConfig` for the Kover-based successor and " + + "`.agents/skills/raise-coverage/references/migrate-to-kover.md` " + + "for the migration recipe.", + level = DeprecationLevel.WARNING +) +@Suppress("DEPRECATION") internal val File.isGenerated get() = this.absolutePath.contains(GENERATED.infix) diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/FileFilter.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/FileFilter.kt index 5b26cc7a64..53793f466d 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/FileFilter.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/FileFilter.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,14 @@ import java.io.File /** * Utilities for filtering the groups of `File`s. */ +@Deprecated( + message = "Used only by the deprecated `JacocoConfig` pipeline. " + + "Generated-code filtering moved to `KoverConfig.applyTo(rootProject)`, " + + "which derives the exclusion list at configuration time and pushes " + + "it into both per-module and root Kover reports. " + + "Removed when `JacocoConfig` is.", + level = DeprecationLevel.WARNING +) internal object FileFilter { /** diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/JacocoConfig.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/JacocoConfig.kt index 06f14c4318..9a7e6e1f39 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/JacocoConfig.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/JacocoConfig.kt @@ -27,6 +27,7 @@ package io.spine.gradle.report.coverage import io.spine.dependency.test.Jacoco +import io.spine.gradle.SpineTaskGroup import io.spine.gradle.applyPlugin import io.spine.gradle.getTask import io.spine.gradle.report.coverage.TaskName.check @@ -62,10 +63,19 @@ import org.gradle.testing.jacoco.tasks.JacocoReport * In a single-module Gradle project, this utility is NOT needed. Just a plain `jacoco` plugin * applied to the project is sufficient. * - * Therefore, tn case this utility is applied to a single-module Gradle project, + * Therefore, in case this utility is applied to a single-module Gradle project, * an `IllegalStateException` is thrown. */ -@Suppress("unused") +@Deprecated( + message = "Use `KoverConfig.applyTo(rootProject)`, the Kover-based " + + "successor that aggregates per-subproject coverage into the " + + "root `koverXmlReport` and excludes classes compiled from " + + "`generated/` source directories. " + + "The `raise-coverage` skill performs this migration automatically. " + + "See .agents/skills/raise-coverage/references/migrate-to-kover.md.", + level = DeprecationLevel.WARNING +) +@Suppress("unused", "DEPRECATION") class JacocoConfig( private val rootProject: Project, private val reportsDir: File, @@ -163,6 +173,8 @@ class JacocoConfig( val humanProducedCompiledFiles = filter.humanProducedCompiledFiles() val rootReport = tasks.register(jacocoRootReport.name, JacocoReport::class.java) { + group = SpineTaskGroup.name + description = "Aggregates JaCoCo coverage data from subprojects into a single report" dependsOn(copyReports) additionalSourceDirs.from(humanProducedSourceFolders) @@ -194,6 +206,8 @@ class JacocoConfig( val originalLocation = rootProject.files(everyExecData) val copyReports = tasks.register(copyReports.name, Copy::class.java) { + group = SpineTaskGroup.name + description = "Copies JaCoCo `.exec` files from subprojects into root reports folder" from(originalLocation) into(reportsDir) rename { diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/KoverConfig.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/KoverConfig.kt new file mode 100644 index 0000000000..4f9e7c8fa1 --- /dev/null +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/KoverConfig.kt @@ -0,0 +1,329 @@ +/* + * 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.coverage + +import io.spine.dependency.test.Jacoco +import io.spine.dependency.test.Kover +import java.io.File +import kotlinx.kover.gradle.plugin.dsl.KoverProjectExtension +import org.gradle.api.Project +import org.gradle.api.file.SourceDirectorySet +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.SourceSet +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet + +private const val GENERATED_MARKER: String = "generated" +private const val KOTLIN_SOURCE_SET_EXT_NAME: String = "kotlin" +private const val KOTLIN_MAIN_SOURCE_SET_SUFFIX: String = "Main" +private const val JAVA_SOURCE_SUFFIX: String = ".java" +private const val KOTLIN_SOURCE_SUFFIX: String = ".kt" +private const val PROTO_KOTLIN_SUFFIX: String = ".proto.kt" +private const val KOTLIN_FILE_CLASS_SUFFIX: String = "Kt" + +/** + * Configures Kover at the root of a multi-module Gradle project to aggregate + * coverage across subprojects and exclude classes that originate from + * `generated/` source directories. + * + * Apply once from the root build script, at top level: + * ``` + * KoverConfig.applyTo(rootProject) + * ``` + * + * Do **not** wrap this call in `gradle.projectsEvaluated { โ€ฆ }`. The Kover + * plugin registers its own `afterEvaluate` hooks at apply time; applying it + * after the root project has been evaluated fails with `Cannot run + * Project.afterEvaluate(Action) when the project is already evaluated`. + * + * Subproject wiring is deferred via `pluginManager.withPlugin(...)`: + * the per-subproject `useJacoco(...)`, aggregation dependency, and exclude + * filter are registered the moment a subproject applies the Kover plugin โ€” + * either immediately (if the plugin is already applied) or later in the + * same configuration phase. Both branches run **before** Kover's own + * `afterEvaluate` finalization, so the engine pin and the aggregation + * dependency are visible when Kover builds its task graph. + * + * Generated-class FQN discovery is resolved lazily through a [Provider] + * passed to `classes(...)`. The directory walk happens at task-graph time + * (not at configuration time), so `protoc`-generated sources created by + * upstream tasks are picked up correctly on a clean build. + * + * The configuration: + * + * - Applies the Kover plugin to the root project. + * - Pins the coverage engine to the JaCoCo version declared in + * [io.spine.dependency.test.Jacoco] via `useJacoco(...)` on the root **and + * on every eligible subproject**. The `jvm-module` / `kmp-module` script + * plugins already pin the same version, so the per-subproject call is + * idempotent for those modules; it matters for subprojects that apply + * Kover directly without the convention plugin. + * - For every subproject that applies Kover, adds a `kover(project(...))` + * dependency so the subproject's coverage flows into the root rollup, + * and pushes the subproject's generated-class FQNs into its own + * `kover { reports { filters { excludes { classes(...) } } } }`. + * - Configures the root `koverXmlReport` task with `onCheck = true` and + * excludes the union of generated-class FQNs across all subprojects. + * + * This is the Kover-based successor to the deprecated JaCoCo-based + * coverage aggregation pipeline. The behaviour mirrors what + * the former JaCoCo-based pipeline provided, but is wired through Kover + * (`koverXmlReport`) instead of vanilla `jacocoRootReport`. + */ +@Suppress("unused") +class KoverConfig private constructor( + private val rootProject: Project, +) { + + companion object { + + /** + * Configures Kover aggregation and generated-code exclusion at the + * root of a multi-module Gradle project. + * + * Must be called with the root project; throws an + * [IllegalArgumentException] if called with a non-root project, and + * an [IllegalStateException] if [project] has no subprojects โ€” + * a single-module Gradle project does not need root aggregation, + * so apply the `jvm-module` / `kmp-module` script plugin (or the + * Kover plugin) directly to that module instead. + * + * Eligibility is determined per subproject: only subprojects that + * apply the Kover plugin (directly or through `jvm-module` / + * `kmp-module`) are wired into the rollup. Subprojects that apply + * Kover after `applyTo` returns are still picked up โ€” wiring runs + * inside a `pluginManager.withPlugin(...)` callback that fires + * the moment the plugin is applied. + */ + fun applyTo(project: Project) { + require(project == project.rootProject) { + "`KoverConfig.applyTo` must be called with the root project. " + + "Received ${project.path}." + } + check(project.subprojects.isNotEmpty()) { + "In a single-module Gradle project, `KoverConfig` is NOT needed. " + + "Apply the Kover plugin directly to the module instead." + } + project.pluginManager.apply(Kover.id) + KoverConfig(project).configure() + } + } + + private fun configure() { + configureRoot() + rootProject.subprojects.forEach { sub -> + sub.pluginManager.withPlugin(Kover.id) { + addAggregationDependency(sub) + configureSubproject(sub) + } + } + } + + private fun addAggregationDependency(sub: Project) { + rootProject.dependencies.add("kover", rootProject.project(sub.path)) + } + + /** + * Pins the coverage engine to the JaCoCo version declared in + * [io.spine.dependency.test.Jacoco] on [sub] and registers a lazy + * exclude filter that resolves [sub]'s generated-class FQNs at + * task-graph time, after upstream code-generation tasks have run. + * + * Calling `useJacoco(...)` is idempotent: the `jvm-module` and + * `kmp-module` script plugins already pin the same version; the call + * here matters for subprojects that apply Kover directly. + */ + private fun configureSubproject(sub: Project) { + sub.extensions.configure(KoverProjectExtension::class.java) { + useJacoco(Jacoco.version) + reports { + filters { + excludes { + classes(perSubprojectExcludePatternsProvider(sub)) + } + } + } + } + } + + private fun configureRoot() { + rootProject.extensions.configure(KoverProjectExtension::class.java) { + useJacoco(Jacoco.version) + reports { + total { + xml { + onCheck.set(true) + } + } + filters { + excludes { + classes(generatedExcludePatternsProvider()) + } + } + } + } + } + + /** + * Lazy `Provider` of the union of generated-class FQN exclusion patterns + * across every subproject that applies the Kover plugin. + * + * Resolved at task-graph time; the per-subproject FQN walk runs **after** + * `protoc` (and other code-generation tasks) have populated each + * subproject's `generated/` directories on a clean build. + */ + private fun generatedExcludePatternsProvider(): Provider> = + rootProject.provider { + rootProject.subprojects.asSequence() + .filter { it.pluginManager.hasPlugin(Kover.id) } + .flatMap { generatedClassFqns(it).asSequence() } + .toSortedSet() + .toExclusionPatterns() + } + + /** + * Lazy `Provider` of the generated-class FQN exclusion patterns + * for [sub]. See [generatedExcludePatternsProvider] for timing notes. + */ + private fun perSubprojectExcludePatternsProvider( + sub: Project, + ): Provider> = + sub.provider { + generatedClassFqns(sub).toSortedSet().toExclusionPatterns() + } + + /** + * Returns the fully-qualified names of all classes that originate from + * `generated/` source directories of the [project]'s production source sets. + * + * Java/Kotlin-JVM projects expose these dirs through the `main` source set. + * Kotlin Multiplatform projects expose them through source sets such as + * `commonMain` and `jvmMain`. + */ + private fun generatedClassFqns(project: Project): List { + return generatedSrcDirs(project) + .asSequence() + .filter { it.exists() && it.isDirectory } + .flatMap { root -> + root.walk() + .filter { !it.isDirectory } + .flatMap { it.fqnsRelativeTo(root).asSequence() } + } + .distinct() + .toList() + } +} + +private fun generatedSrcDirs(project: Project): Set { + val javaDirs = javaMainSourceSet(project) + ?.let(::generatedSrcDirs) + ?: emptySet() + val kotlinMultiplatformDirs = kotlinMultiplatformMainSourceSets(project) + .asSequence() + .flatMap { generatedSrcDirs(it).asSequence() } + .toSet() + return javaDirs + kotlinMultiplatformDirs +} + +private fun javaMainSourceSet(project: Project): SourceSet? = + project.extensions.findByType(JavaPluginExtension::class.java) + ?.sourceSets + ?.findByName(SourceSet.MAIN_SOURCE_SET_NAME) + +private fun kotlinMultiplatformMainSourceSets(project: Project): List = + project.extensions.findByType(KotlinMultiplatformExtension::class.java) + ?.sourceSets + ?.filter { it.isMainSourceSet() } + ?: emptyList() + +private fun generatedSrcDirs(main: SourceSet): Set { + val javaDirs = main.allJava.srcDirs + val kotlinDirs = + (main.extensions.findByName(KOTLIN_SOURCE_SET_EXT_NAME) as? SourceDirectorySet) + ?.srcDirs + ?: emptySet() + return (javaDirs + kotlinDirs).filter { it.absolutePath.contains(GENERATED_MARKER) } + .toSet() +} + +@OptIn(ExperimentalKotlinGradlePluginApi::class) +private fun generatedSrcDirs(sourceSet: KotlinSourceSet): Set { + val kotlinDirs = sourceSet.kotlin.srcDirs + val generatedKotlinDirs = sourceSet.generatedKotlin.srcDirs + return (kotlinDirs + generatedKotlinDirs) + .filter { it.absolutePath.contains(GENERATED_MARKER) } + .toSet() +} + +private fun KotlinSourceSet.isMainSourceSet(): Boolean = + name == KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME || + name.endsWith(KOTLIN_MAIN_SOURCE_SET_SUFFIX) + +/** + * Derives one or more class FQNs from this source file's path relative + * to [root]. + * + * - `.java` โ€” one FQN. + * - `.kt` โ€” the declared class plus the Kotlin file-class synthetic + * (`Kt`). + * - `.proto.kt` โ€” `protoc-gen-kotlin` convention; the two-part suffix + * is stripped, otherwise treated as a `.kt` file. + * - any other extension โ€” an empty list. + * + * Returns an empty list if this file is not under [root]. + */ +private fun File.fqnsRelativeTo(root: File): List { + if (!startsWith(root)) { + return emptyList() + } + val relative = toRelativeString(root) + return when { + relative.endsWith(PROTO_KOTLIN_SUFFIX) -> { + val base = relative.removeSuffix(PROTO_KOTLIN_SUFFIX).toFqn() + listOf(base, base + KOTLIN_FILE_CLASS_SUFFIX) + } + relative.endsWith(KOTLIN_SOURCE_SUFFIX) -> { + val base = relative.removeSuffix(KOTLIN_SOURCE_SUFFIX).toFqn() + listOf(base, base + KOTLIN_FILE_CLASS_SUFFIX) + } + relative.endsWith(JAVA_SOURCE_SUFFIX) -> + listOf(relative.removeSuffix(JAVA_SOURCE_SUFFIX).toFqn()) + else -> emptyList() + } +} + +/** + * Expands each fully-qualified class name into two Kover exclusion + * patterns: the class itself, and `$*` for any nested or anonymous + * classes the compiler emits alongside it. + */ +private fun Collection.toExclusionPatterns(): List = + flatMap { listOf(it, "$it\$*") } + +private fun String.toFqn(): String = replace(File.separatorChar, '.') diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/PathMarker.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/PathMarker.kt index f91b83f17d..9eccd6f998 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/PathMarker.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/PathMarker.kt @@ -29,6 +29,14 @@ package io.spine.gradle.report.coverage /** * Fragments of file path which allow to detect the type of the file. */ +@Deprecated( + message = "Used only by the deprecated `JacocoConfig` pipeline. " + + "Removed when `JacocoConfig` is. " + + "See `KoverConfig` for the Kover-based successor and " + + "`.agents/skills/raise-coverage/references/migrate-to-kover.md` " + + "for the migration recipe.", + level = DeprecationLevel.WARNING +) internal enum class PathMarker(val infix: String) { /** diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/TaskName.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/TaskName.kt index 7c0e386dd1..ad13dac5f2 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/TaskName.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/TaskName.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,15 @@ package io.spine.gradle.report.coverage /** * The names of Gradle tasks involved in the JaCoCo reporting. */ +@Deprecated( + message = "Internal task-name catalog for the deprecated `JacocoConfig` pipeline. " + + "Kover uses its built-in task names (`koverXmlReport`, etc.). " + + "Removed when `JacocoConfig` is. " + + "See `KoverConfig` for the Kover-based successor and " + + "`.agents/skills/raise-coverage/references/migrate-to-kover.md` " + + "for the migration recipe.", + level = DeprecationLevel.WARNING +) @Suppress("EnumEntryName", "EnumNaming") /* Dubbing the actual values in Gradle. */ internal enum class TaskName { jacocoRootReport, 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 14c280b7df..49267d6e19 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 @@ -30,6 +30,7 @@ import com.github.jk1.license.LicenseReportExtension import com.github.jk1.license.LicenseReportExtension.ALL import com.github.jk1.license.LicenseReportPlugin import io.spine.dependency.local.Spine +import io.spine.gradle.SpineTaskGroup import io.spine.gradle.applyPlugin import io.spine.gradle.getTask import java.io.File @@ -109,6 +110,8 @@ object LicenseReporter { fun mergeAllReports(project: Project) { val rootProject = project.rootProject val mergeTask = rootProject.tasks.register(mergeTaskName) { + group = SpineTaskGroup.name + description = "Merges per-project license reports into a single repository-wide report" val consolidationTask = this val assembleTask = project.getTask("assemble") val sourceProjects: Iterable = sourceProjects(rootProject) diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomGenerator.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomGenerator.kt index 7ffeda1896..9ecb36244c 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomGenerator.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomGenerator.kt @@ -26,6 +26,7 @@ package io.spine.gradle.report.pom +import io.spine.gradle.SpineTaskGroup import io.spine.gradle.report.license.Paths import org.gradle.api.Project import org.gradle.api.plugins.BasePlugin @@ -83,6 +84,8 @@ object PomGenerator { } val task = project.tasks.register("generatePom") { + group = SpineTaskGroup.name + description = "Generates a `pom.xml` file describing project dependencies" doLast { val pomFile = Paths.outputFile(project.rootDir, pomFilename) pomFile.parentFile.mkdirs() diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/testing/Tasks.kt b/buildSrc/src/main/kotlin/io/spine/gradle/testing/Tasks.kt index 30ac810eae..41f07f09cf 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/testing/Tasks.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/testing/Tasks.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. @@ -26,6 +26,7 @@ package io.spine.gradle.testing +import io.spine.gradle.SpineTaskGroup import org.gradle.api.tasks.TaskContainer import org.gradle.api.tasks.testing.Test import org.gradle.kotlin.dsl.register @@ -79,7 +80,7 @@ private const val SLOW_TAG = "slow" private abstract class FastTest : Test() { init { description = "Executes all JUnit tests but the ones tagged as `slow`." - group = "Verification" + group = SpineTaskGroup.name this.useJUnitPlatform { excludeTags(SLOW_TAG) @@ -93,7 +94,7 @@ private abstract class FastTest : Test() { private abstract class SlowTest : Test() { init { description = "Executes JUnit tests tagged as `slow`." - group = "Verification" + group = SpineTaskGroup.name // No slow tests -- no problem. filter.isFailOnNoMatchingTests = false this.useJUnitPlatform { diff --git a/buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts b/buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts index de7f1bfffc..7334ef97f0 100644 --- a/buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts +++ b/buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts @@ -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. @@ -30,10 +30,25 @@ import org.gradle.kotlin.dsl.getting import org.gradle.kotlin.dsl.jacoco import org.gradle.testing.jacoco.tasks.JacocoReport +// DEPRECATED: this script plugin distributes vanilla JaCoCo. +// New code should apply `kmp-module`, which configures Kover via +// `useJacoco(version = Jacoco.version)` and writes JaCoCo-format XML at +// `build/reports/kover/report.xml`. (Same task and path as Kotlin-JVM โ€” +// `kmp-module` configures only Kover's `total` report, so no +// `koverXmlReport` task is generated.) The `raise-coverage` skill +// migrates existing consumers automatically. Kept so older consumer repos +// continue to build; will be removed in a future release. +// See: .agents/skills/raise-coverage/references/migrate-to-kover.md + plugins { jacoco } +logger.warn( + "'jacoco-kmm-jvm' is deprecated; use 'kmp-module' which applies Kover. " + + "See .agents/skills/raise-coverage/references/migrate-to-kover.md." +) + /** * Configures [JacocoReport] task to run in a Kotlin KMM project for `commonMain` and `jvmMain` * source sets. diff --git a/buildSrc/src/main/kotlin/jacoco-kotlin-jvm.gradle.kts b/buildSrc/src/main/kotlin/jacoco-kotlin-jvm.gradle.kts index 48fb126e92..185c9cdfdd 100644 --- a/buildSrc/src/main/kotlin/jacoco-kotlin-jvm.gradle.kts +++ b/buildSrc/src/main/kotlin/jacoco-kotlin-jvm.gradle.kts @@ -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. @@ -26,10 +26,23 @@ import io.spine.gradle.buildDirectory +// DEPRECATED: this script plugin distributes vanilla JaCoCo. +// New code should apply `jvm-module`, which configures Kover via +// `useJacoco(version = Jacoco.version)` and writes JaCoCo-format XML at +// `build/reports/kover/report.xml`. The `raise-coverage` skill migrates +// existing consumers automatically. Kept so older consumer repos continue to +// build; will be removed in a future release. +// See: .agents/skills/raise-coverage/references/migrate-to-kover.md + plugins { jacoco } +logger.warn( + "'jacoco-kotlin-jvm' is deprecated; use 'jvm-module' which applies Kover. " + + "See .agents/skills/raise-coverage/references/migrate-to-kover.md." +) + /** * Configures [JacocoReport] task to run in a Kotlin Multiplatform project for * `commonMain` and `jvmMain` source sets. diff --git a/buildSrc/src/main/kotlin/jvm-module.gradle.kts b/buildSrc/src/main/kotlin/jvm-module.gradle.kts index a7b2cd44ae..a7b3113092 100644 --- a/buildSrc/src/main/kotlin/jvm-module.gradle.kts +++ b/buildSrc/src/main/kotlin/jvm-module.gradle.kts @@ -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. @@ -35,6 +35,7 @@ import io.spine.dependency.lib.Kotlin import io.spine.dependency.lib.Protobuf import io.spine.dependency.local.Reflect import io.spine.dependency.test.Jacoco +import io.spine.gradle.SpineTaskGroup import io.spine.gradle.checkstyle.CheckStyleConfig import io.spine.gradle.github.pages.updateGitHubPages import io.spine.gradle.javac.configureErrorProne @@ -150,6 +151,8 @@ fun Module.forceConfigurations() { fun Module.setTaskDependencies(generatedDir: String) { tasks { val cleanGenerated by registering(Delete::class) { + group = SpineTaskGroup.name + description = "Deletes the directory with generated sources" delete(generatedDir) } clean.configure { diff --git a/buildSrc/src/main/kotlin/kmp-module.gradle.kts b/buildSrc/src/main/kotlin/kmp-module.gradle.kts index 9bc0fd34b3..a2e6d82e58 100644 --- a/buildSrc/src/main/kotlin/kmp-module.gradle.kts +++ b/buildSrc/src/main/kotlin/kmp-module.gradle.kts @@ -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. diff --git a/buildSrc/src/main/kotlin/uber-jar-module.gradle.kts b/buildSrc/src/main/kotlin/uber-jar-module.gradle.kts index f3dda52167..0cace2ebe3 100644 --- a/buildSrc/src/main/kotlin/uber-jar-module.gradle.kts +++ b/buildSrc/src/main/kotlin/uber-jar-module.gradle.kts @@ -29,6 +29,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import io.spine.gradle.publish.IncrementGuard import io.spine.gradle.publish.SpinePublishing +import io.spine.gradle.publish.setup import io.spine.gradle.publish.spinePublishing import io.spine.gradle.report.license.LicenseReporter @@ -44,7 +45,10 @@ apply() LicenseReporter.generateReportIn(project) spinePublishing { + // This prefix does not apply to the modules of this project because they all belong + // to the `io.spine.tools` group, and therefore `toolArtifactPrefix` applies instead. artifactPrefix = "" + toolArtifactPrefix = "NONE" destinations = rootProject.the().destinations customPublishing = true } @@ -84,8 +88,9 @@ tasks.publish { } tasks.shadowJar { + setup() excludeFiles() - setZip64(true) /* The archive has way too many items. So using the Zip64 mode. */ + isZip64 = true /* The archive has way too many items. So using the Zip64 mode. */ archiveClassifier.set("") /** To prevent Gradle setting something like `osx-x86_64`. */ } diff --git a/buildSrc/src/main/kotlin/write-manifest.gradle.kts b/buildSrc/src/main/kotlin/write-manifest.gradle.kts index b63d3272da..49130c0c4b 100644 --- a/buildSrc/src/main/kotlin/write-manifest.gradle.kts +++ b/buildSrc/src/main/kotlin/write-manifest.gradle.kts @@ -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. @@ -24,6 +24,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +import io.spine.gradle.SpineTaskGroup import io.spine.gradle.publish.SpinePublishing import java.nio.file.Files.createDirectories import java.nio.file.Files.createFile @@ -105,6 +106,9 @@ val manifestAttributes = mapOf( */ val exposeManifestForTests by tasks.registering { + group = SpineTaskGroup.name + description = "Writes a `MANIFEST.MF` to `resources/main` so that it is visible to tests" + val outputFile = layout.buildDirectory.file("resources/main/META-INF/MANIFEST.MF") outputs.file(outputFile).withPropertyName("manifestFile") diff --git a/config b/config index bf604a9683..527f658daa 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit bf604a96834ebf062335a24fcec346e259dd649c +Subproject commit 527f658daa8d54ff5ab31cdcaf7b6f94afe7b8a0 diff --git a/dependencies.md b/dependencies.md index e6a0426694..0f13835e2d 100644 --- a/dependencies.md +++ b/dependencies.md @@ -1,17 +1,17 @@ -# Dependencies of `io.spine:spine-annotations:2.0.0-SNAPSHOT.387` +# Dependencies of `io.spine:spine-annotations:2.0.0-SNAPSHOT.384` ## Runtime 1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.0.2. * **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -58,6 +58,10 @@ * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : com.github.ben-manes.caffeine. **Name** : caffeine. **Version** : 2.9.3. + * **Project URL:** [https://github.com/ben-manes/caffeine](https://github.com/ben-manes/caffeine) + * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : com.github.ben-manes.caffeine. **Name** : caffeine. **Version** : 3.0.5. * **Project URL:** [https://github.com/ben-manes/caffeine](https://github.com/ben-manes/caffeine) * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -143,15 +147,15 @@ * **Project URL:** [https://github.com/google/j2objc/](https://github.com/google/j2objc/) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) @@ -334,6 +338,14 @@ * **Project URL:** [https://github.com/kotest/kotest](https://github.com/kotest/kotest) * **License:** [Apache-2.0](https://opensource.org/licenses/Apache-2.0) +1. **Group** : io.opentelemetry. **Name** : opentelemetry-api. **Version** : 1.41.0. + * **Project URL:** [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : io.opentelemetry. **Name** : opentelemetry-context. **Version** : 1.41.0. + * **Project URL:** [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : javax.inject. **Name** : javax.inject. **Version** : 1. * **Project URL:** [http://code.google.com/p/atinject/](http://code.google.com/p/atinject/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -384,6 +396,23 @@ * **Project URL:** [https://github.com/apiguardian-team/apiguardian](https://github.com/apiguardian-team/apiguardian) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.bouncycastle. **Name** : bcpg-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcpkix-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcprov-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcutil-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + 1. **Group** : org.checkerframework. **Name** : checker-compat-qual. **Version** : 2.5.3. * **Project URL:** [https://checkerframework.org](https://checkerframework.org) * **License:** [GNU General Public License, version 2 (GPL2), with the classpath exception](http://www.gnu.org/software/classpath/license.html) @@ -473,31 +502,23 @@ * **Project URL:** [https://github.com/JetBrains/intellij-deps-trove4j](https://github.com/JetBrains/intellij-deps-trove4j) * **License:** [GNU LESSER GENERAL PUBLIC LICENSE 2.1](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html) -1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools-api. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools-api. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-api. **Version** : 2.3.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-compat. **Version** : 2.3.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-cri-impl. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-api. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-impl. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-impl. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -505,15 +526,15 @@ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-runner. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-runner. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-client. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-client. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -521,19 +542,15 @@ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-abi-reader. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-commonizer-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-commonizer-embeddable. **Version** : 2.3.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-metadata-jvm. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-metadata-jvm. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -541,7 +558,7 @@ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -549,23 +566,23 @@ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-script-runtime. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-script-runtime. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-common. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-common. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-impl-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-impl-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-jvm. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-jvm. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -573,7 +590,7 @@ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -581,7 +598,7 @@ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-common. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-common. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -589,7 +606,7 @@ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk7. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk7. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -597,14 +614,18 @@ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk8. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk8. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-tooling-core. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : swift-export-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : atomicfu. **Version** : 0.23.1. + * **Project URL:** [https://github.com/Kotlin/kotlinx.atomicfu](https://github.com/Kotlin/kotlinx.atomicfu) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : atomicfu. **Version** : 0.29.0. * **Project URL:** [https://github.com/Kotlin/kotlinx.atomicfu](https://github.com/Kotlin/kotlinx.atomicfu) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -657,14 +678,26 @@ * **Project URL:** [https://github.com/Kotlin/kotlinx.html](https://github.com/Kotlin/kotlinx.html) * **License:** [The Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-bom. **Version** : 1.7.3. + * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core. **Version** : 1.7.3. + * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core-jvm. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core-jvm. **Version** : 1.7.3. + * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-json. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -681,7 +714,7 @@ * **Project URL:** [http://jspecify.org/](http://jspecify.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.junit. **Name** : junit-bom. **Version** : 6.0.3. +1. **Group** : org.junit. **Name** : junit-bom. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) @@ -689,27 +722,27 @@ * **Project URL:** [https://junit-pioneer.org/](https://junit-pioneer.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-api. **Version** : 6.0.3. +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-api. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-engine. **Version** : 6.0.3. +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-engine. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-params. **Version** : 6.0.3. +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-params. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-commons. **Version** : 6.0.3. +1. **Group** : org.junit.platform. **Name** : junit-platform-commons. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-engine. **Version** : 6.0.3. +1. **Group** : org.junit.platform. **Name** : junit-platform-engine. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-launcher. **Version** : 6.0.3. +1. **Group** : org.junit.platform. **Name** : junit-platform-launcher. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) @@ -764,14 +797,14 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using +This report was generated on **Mon Dec 22 14:00:41 WET 2025** 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:spine-base:2.0.0-SNAPSHOT.387` +# Dependencies of `io.spine:spine-base:2.0.0-SNAPSHOT.384` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -801,15 +834,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/google/j2objc/](https://github.com/google/j2objc/) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) @@ -817,15 +850,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -877,6 +910,10 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : com.github.ben-manes.caffeine. **Name** : caffeine. **Version** : 2.9.3. + * **Project URL:** [https://github.com/ben-manes/caffeine](https://github.com/ben-manes/caffeine) + * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : com.github.ben-manes.caffeine. **Name** : caffeine. **Version** : 3.0.5. * **Project URL:** [https://github.com/ben-manes/caffeine](https://github.com/ben-manes/caffeine) * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -909,11 +946,11 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/google/gson](https://github.com/google/gson) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.devtools.ksp. **Name** : symbol-processing. **Version** : 2.3.6. +1. **Group** : com.google.devtools.ksp. **Name** : symbol-processing. **Version** : 2.3.0. * **Project URL:** [https://goo.gle/ksp](https://goo.gle/ksp) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.devtools.ksp. **Name** : symbol-processing-api. **Version** : 2.3.6. +1. **Group** : com.google.devtools.ksp. **Name** : symbol-processing-api. **Version** : 2.3.0. * **Project URL:** [https://goo.gle/ksp](https://goo.gle/ksp) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -970,19 +1007,19 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/google/j2objc/](https://github.com/google/j2objc/) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protoc. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protoc. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1178,6 +1215,14 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/kotest/kotest](https://github.com/kotest/kotest) * **License:** [Apache-2.0](https://opensource.org/licenses/Apache-2.0) +1. **Group** : io.opentelemetry. **Name** : opentelemetry-api. **Version** : 1.41.0. + * **Project URL:** [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : io.opentelemetry. **Name** : opentelemetry-context. **Version** : 1.41.0. + * **Project URL:** [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : javax.inject. **Name** : javax.inject. **Version** : 1. * **Project URL:** [http://code.google.com/p/atinject/](http://code.google.com/p/atinject/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1236,6 +1281,23 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/apiguardian-team/apiguardian](https://github.com/apiguardian-team/apiguardian) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.bouncycastle. **Name** : bcpg-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcpkix-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcprov-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcutil-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + 1. **Group** : org.checkerframework. **Name** : checker-compat-qual. **Version** : 2.5.3. * **Project URL:** [https://checkerframework.org](https://checkerframework.org) * **License:** [GNU General Public License, version 2 (GPL2), with the classpath exception](http://www.gnu.org/software/classpath/license.html) @@ -1325,31 +1387,23 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/JetBrains/intellij-deps-trove4j](https://github.com/JetBrains/intellij-deps-trove4j) * **License:** [GNU LESSER GENERAL PUBLIC LICENSE 2.1](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html) -1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools-api. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools-api. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-api. **Version** : 2.3.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-compat. **Version** : 2.3.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-cri-impl. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-api. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-impl. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-impl. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1357,15 +1411,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-runner. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-runner. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-client. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-client. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1373,19 +1427,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-abi-reader. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-commonizer-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-commonizer-embeddable. **Version** : 2.3.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-metadata-jvm. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-metadata-jvm. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1393,7 +1443,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1401,23 +1451,23 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-script-runtime. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-script-runtime. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-common. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-common. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-impl-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-impl-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-jvm. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-jvm. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1425,7 +1475,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1433,7 +1483,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-common. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-common. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1441,7 +1491,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk7. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk7. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1449,14 +1499,18 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk8. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk8. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-tooling-core. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : swift-export-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : atomicfu. **Version** : 0.23.1. + * **Project URL:** [https://github.com/Kotlin/kotlinx.atomicfu](https://github.com/Kotlin/kotlinx.atomicfu) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : atomicfu. **Version** : 0.29.0. * **Project URL:** [https://github.com/Kotlin/kotlinx.atomicfu](https://github.com/Kotlin/kotlinx.atomicfu) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1509,14 +1563,26 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/Kotlin/kotlinx.html](https://github.com/Kotlin/kotlinx.html) * **License:** [The Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-bom. **Version** : 1.7.3. + * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core. **Version** : 1.7.3. + * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core-jvm. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core-jvm. **Version** : 1.7.3. + * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-json. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1533,7 +1599,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [http://jspecify.org/](http://jspecify.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.junit. **Name** : junit-bom. **Version** : 6.0.3. +1. **Group** : org.junit. **Name** : junit-bom. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) @@ -1541,27 +1607,27 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://junit-pioneer.org/](https://junit-pioneer.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-api. **Version** : 6.0.3. +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-api. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-engine. **Version** : 6.0.3. +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-engine. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-params. **Version** : 6.0.3. +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-params. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-commons. **Version** : 6.0.3. +1. **Group** : org.junit.platform. **Name** : junit-platform-commons. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-engine. **Version** : 6.0.3. +1. **Group** : org.junit.platform. **Name** : junit-platform-engine. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-launcher. **Version** : 6.0.3. +1. **Group** : org.junit.platform. **Name** : junit-platform-launcher. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) @@ -1616,14 +1682,14 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using +This report was generated on **Mon Dec 22 14:00:42 WET 2025** 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:spine-environment:2.0.0-SNAPSHOT.387` +# Dependencies of `io.spine:spine-environment:2.0.0-SNAPSHOT.384` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -1653,15 +1719,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/google/j2objc/](https://github.com/google/j2objc/) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) @@ -1669,15 +1735,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1740,6 +1806,10 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : com.github.ben-manes.caffeine. **Name** : caffeine. **Version** : 2.9.3. + * **Project URL:** [https://github.com/ben-manes/caffeine](https://github.com/ben-manes/caffeine) + * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : com.github.ben-manes.caffeine. **Name** : caffeine. **Version** : 3.0.5. * **Project URL:** [https://github.com/ben-manes/caffeine](https://github.com/ben-manes/caffeine) * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1825,15 +1895,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/google/j2objc/](https://github.com/google/j2objc/) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) @@ -2016,6 +2086,14 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/kotest/kotest](https://github.com/kotest/kotest) * **License:** [Apache-2.0](https://opensource.org/licenses/Apache-2.0) +1. **Group** : io.opentelemetry. **Name** : opentelemetry-api. **Version** : 1.41.0. + * **Project URL:** [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : io.opentelemetry. **Name** : opentelemetry-context. **Version** : 1.41.0. + * **Project URL:** [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : javax.inject. **Name** : javax.inject. **Version** : 1. * **Project URL:** [http://code.google.com/p/atinject/](http://code.google.com/p/atinject/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2066,6 +2144,23 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/apiguardian-team/apiguardian](https://github.com/apiguardian-team/apiguardian) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.bouncycastle. **Name** : bcpg-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcpkix-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcprov-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcutil-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + 1. **Group** : org.checkerframework. **Name** : checker-compat-qual. **Version** : 2.5.3. * **Project URL:** [https://checkerframework.org](https://checkerframework.org) * **License:** [GNU General Public License, version 2 (GPL2), with the classpath exception](http://www.gnu.org/software/classpath/license.html) @@ -2155,31 +2250,23 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/JetBrains/intellij-deps-trove4j](https://github.com/JetBrains/intellij-deps-trove4j) * **License:** [GNU LESSER GENERAL PUBLIC LICENSE 2.1](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html) -1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools-api. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools-api. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-api. **Version** : 2.3.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-compat. **Version** : 2.3.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-cri-impl. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-api. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-impl. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-impl. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2187,15 +2274,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-runner. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-runner. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-client. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-client. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2203,19 +2290,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-abi-reader. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-commonizer-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-commonizer-embeddable. **Version** : 2.3.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-metadata-jvm. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-metadata-jvm. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2223,7 +2306,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2231,23 +2314,23 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-script-runtime. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-script-runtime. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-common. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-common. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-impl-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-impl-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-jvm. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-jvm. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2255,7 +2338,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2263,7 +2346,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-common. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-common. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2271,7 +2354,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk7. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk7. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2279,14 +2362,18 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk8. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk8. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-tooling-core. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : swift-export-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : atomicfu. **Version** : 0.23.1. + * **Project URL:** [https://github.com/Kotlin/kotlinx.atomicfu](https://github.com/Kotlin/kotlinx.atomicfu) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : atomicfu. **Version** : 0.29.0. * **Project URL:** [https://github.com/Kotlin/kotlinx.atomicfu](https://github.com/Kotlin/kotlinx.atomicfu) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2339,14 +2426,26 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/Kotlin/kotlinx.html](https://github.com/Kotlin/kotlinx.html) * **License:** [The Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-bom. **Version** : 1.7.3. + * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core. **Version** : 1.7.3. + * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core-jvm. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core-jvm. **Version** : 1.7.3. + * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-json. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2363,7 +2462,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [http://jspecify.org/](http://jspecify.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.junit. **Name** : junit-bom. **Version** : 6.0.3. +1. **Group** : org.junit. **Name** : junit-bom. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) @@ -2371,27 +2470,27 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://junit-pioneer.org/](https://junit-pioneer.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-api. **Version** : 6.0.3. +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-api. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-engine. **Version** : 6.0.3. +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-engine. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-params. **Version** : 6.0.3. +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-params. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-commons. **Version** : 6.0.3. +1. **Group** : org.junit.platform. **Name** : junit-platform-commons. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-engine. **Version** : 6.0.3. +1. **Group** : org.junit.platform. **Name** : junit-platform-engine. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-launcher. **Version** : 6.0.3. +1. **Group** : org.junit.platform. **Name** : junit-platform-launcher. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) @@ -2446,14 +2545,14 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using +This report was generated on **Mon Dec 22 14:00:41 WET 2025** 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:spine-format:2.0.0-SNAPSHOT.387` +# Dependencies of `io.spine:spine-format:2.0.0-SNAPSHOT.384` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.20.0. @@ -2527,15 +2626,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/google/j2objc/](https://github.com/google/j2objc/) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) @@ -2543,15 +2642,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2646,6 +2745,10 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : com.github.ben-manes.caffeine. **Name** : caffeine. **Version** : 2.9.3. + * **Project URL:** [https://github.com/ben-manes/caffeine](https://github.com/ben-manes/caffeine) + * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : com.github.ben-manes.caffeine. **Name** : caffeine. **Version** : 3.0.5. * **Project URL:** [https://github.com/ben-manes/caffeine](https://github.com/ben-manes/caffeine) * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2731,15 +2834,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/google/j2objc/](https://github.com/google/j2objc/) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) @@ -2922,6 +3025,14 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/kotest/kotest](https://github.com/kotest/kotest) * **License:** [Apache-2.0](https://opensource.org/licenses/Apache-2.0) +1. **Group** : io.opentelemetry. **Name** : opentelemetry-api. **Version** : 1.41.0. + * **Project URL:** [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : io.opentelemetry. **Name** : opentelemetry-context. **Version** : 1.41.0. + * **Project URL:** [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : javax.inject. **Name** : javax.inject. **Version** : 1. * **Project URL:** [http://code.google.com/p/atinject/](http://code.google.com/p/atinject/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2972,6 +3083,23 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/apiguardian-team/apiguardian](https://github.com/apiguardian-team/apiguardian) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.bouncycastle. **Name** : bcpg-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcpkix-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcprov-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcutil-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + 1. **Group** : org.checkerframework. **Name** : checker-compat-qual. **Version** : 2.5.3. * **Project URL:** [https://checkerframework.org](https://checkerframework.org) * **License:** [GNU General Public License, version 2 (GPL2), with the classpath exception](http://www.gnu.org/software/classpath/license.html) @@ -3061,31 +3189,23 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/JetBrains/intellij-deps-trove4j](https://github.com/JetBrains/intellij-deps-trove4j) * **License:** [GNU LESSER GENERAL PUBLIC LICENSE 2.1](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html) -1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools-api. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools-api. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-api. **Version** : 2.3.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-compat. **Version** : 2.3.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-cri-impl. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-api. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-impl. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-impl. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3093,15 +3213,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-runner. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-runner. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-client. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-client. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3109,19 +3229,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-abi-reader. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-commonizer-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-commonizer-embeddable. **Version** : 2.3.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-metadata-jvm. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-metadata-jvm. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3129,7 +3245,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3137,23 +3253,23 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-script-runtime. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-script-runtime. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-common. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-common. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-impl-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-impl-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-jvm. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-jvm. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3161,7 +3277,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3169,7 +3285,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-common. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-common. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3177,7 +3293,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk7. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk7. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3185,14 +3301,18 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk8. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk8. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-tooling-core. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : swift-export-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : atomicfu. **Version** : 0.23.1. + * **Project URL:** [https://github.com/Kotlin/kotlinx.atomicfu](https://github.com/Kotlin/kotlinx.atomicfu) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : atomicfu. **Version** : 0.29.0. * **Project URL:** [https://github.com/Kotlin/kotlinx.atomicfu](https://github.com/Kotlin/kotlinx.atomicfu) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3245,14 +3365,26 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/Kotlin/kotlinx.html](https://github.com/Kotlin/kotlinx.html) * **License:** [The Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-bom. **Version** : 1.7.3. + * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core. **Version** : 1.7.3. + * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core-jvm. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core-jvm. **Version** : 1.7.3. + * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-json. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3269,7 +3401,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [http://jspecify.org/](http://jspecify.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.junit. **Name** : junit-bom. **Version** : 6.0.3. +1. **Group** : org.junit. **Name** : junit-bom. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) @@ -3277,27 +3409,27 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://junit-pioneer.org/](https://junit-pioneer.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-api. **Version** : 6.0.3. +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-api. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-engine. **Version** : 6.0.3. +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-engine. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-params. **Version** : 6.0.3. +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-params. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-commons. **Version** : 6.0.3. +1. **Group** : org.junit.platform. **Name** : junit-platform-commons. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-engine. **Version** : 6.0.3. +1. **Group** : org.junit.platform. **Name** : junit-platform-engine. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-launcher. **Version** : 6.0.3. +1. **Group** : org.junit.platform. **Name** : junit-platform-launcher. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) @@ -3356,6 +3488,6 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using +This report was generated on **Mon Dec 22 14:00:41 WET 2025** 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 +[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md index 8c92a264b1..5c37a7951c 100644 --- a/docs/dependencies/dependencies.md +++ b/docs/dependencies/dependencies.md @@ -1,6 +1,6 @@ -# Dependencies of `io.spine:spine-annotations:2.0.0-SNAPSHOT.391` +# Dependencies of `io.spine:spine-annotations:2.0.0-SNAPSHOT.392` ## Runtime 1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.0.2. @@ -764,14 +764,14 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu May 28 20:47:29 WEST 2026** using +This report was generated on **Tue Jun 02 22:17:18 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:spine-base:2.0.0-SNAPSHOT.391` +# Dependencies of `io.spine:spine-base:2.0.0-SNAPSHOT.392` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -1616,14 +1616,14 @@ This report was generated on **Thu May 28 20:47:29 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu May 28 20:47:29 WEST 2026** using +This report was generated on **Tue Jun 02 22:17:18 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:spine-environment:2.0.0-SNAPSHOT.391` +# Dependencies of `io.spine:spine-environment:2.0.0-SNAPSHOT.392` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -2446,14 +2446,14 @@ This report was generated on **Thu May 28 20:47:29 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu May 28 20:47:29 WEST 2026** using +This report was generated on **Tue Jun 02 22:17:18 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:spine-format:2.0.0-SNAPSHOT.391` +# Dependencies of `io.spine:spine-format:2.0.0-SNAPSHOT.392` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.21.3. @@ -3356,6 +3356,6 @@ This report was generated on **Thu May 28 20:47:29 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu May 28 20:47:30 WEST 2026** using +This report was generated on **Tue Jun 02 22:17:18 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 e0550a2b31..f7f612abab 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 base-libraries -2.0.0-SNAPSHOT.391 +2.0.0-SNAPSHOT.392 2015 diff --git a/docs/project.md b/docs/project.md new file mode 100644 index 0000000000..a198b69d38 --- /dev/null +++ b/docs/project.md @@ -0,0 +1,38 @@ +# Project: base-libraries + +## Overview + +`base-libraries` is a foundational JVM repository in the Spine SDK organisation. +It hosts the common data types, annotations, environment helpers, and +parsing/serialization utilities that the rest of the Spine SDK (notably +[`core-jvm`](https://github.com/SpineEventEngine/core-java)) depends on. The +artifacts published from this repo sit at the bottom of the Spine dependency +graph, so changes here ripple into most other Spine projects. + +## Architecture + +Role: **library** (multi-module Gradle build) publishing the following Maven +artifacts under the `io.spine` group: + +- `annotations` โ€” annotation types used across the Spine SDK. +- `base` โ€” common data types and utilities. Not consumed directly by + end users; re-exposed as an `api` dependency by `spine-client` and + `spine-server` in `core-jvm`. +- `environment` โ€” runtime environment detection helpers. +- `format` โ€” parsers for YAML, JSON, binary Protobuf, and Protobuf JSON; + used internally by Spine SDK components. + +Key constraints: + +- Public API stability matters: downstream Spine repos pin to versions + published from here, so removals and signature changes are breaking. +- No analytics, telemetry, reflection, or unsafe code (see + `.agents/guidelines/safety-rules.md`). +- Versioning follows the Spine SDK policy (`.agents/guidelines/version-policy.md`); + CI's `Version Guard` rejects branches that reuse a published version. +- Dependency declarations live under + `buildSrc/src/main/kotlin/io/spine/dependency/` and are audited by the + `dependency-audit` skill. + +Read [`.agents/jvm-project.md`](jvm-project.md) for build stack, coding +style, tests, and versioning. diff --git a/environment/src/test/kotlin/io/spine/environment/EnvironmentTypeSpec.kt b/environment/src/test/kotlin/io/spine/environment/EnvironmentTypeSpec.kt new file mode 100644 index 0000000000..b6ac0b4fa1 --- /dev/null +++ b/environment/src/test/kotlin/io/spine/environment/EnvironmentTypeSpec.kt @@ -0,0 +1,53 @@ +/* + * 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.environment + +import com.google.common.testing.EqualsTester +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`EnvironmentType` should") +internal class EnvironmentTypeSpec { + + @Test + fun `obey the 'equals' and 'hashCode' contract`() { + EqualsTester() + .addEqualityGroup(StubType(), StubType()) + .addEqualityGroup(OtherStubType(), OtherStubType()) + .testEquals() + } +} + +private class StubType : EnvironmentType() { + override fun enabled(): Boolean = false + override fun self(): StubType = this +} + +private class OtherStubType : EnvironmentType() { + override fun enabled(): Boolean = false + override fun self(): OtherStubType = this +} diff --git a/version.gradle.kts b/version.gradle.kts index 63e84e8f4a..cce8164f08 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.391") +val versionToPublish: String by extra("2.0.0-SNAPSHOT.392")