From 618deb544f24977e33068fa526cee25cbe464e4a Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 1 Jun 2026 18:31:58 +0100 Subject: [PATCH 01/26] Update `config` --- .agents/_TOC.md | 2 + .agents/documentation-guidelines.md | 21 + .agents/skills/api-discovery/SKILL.md | 20 +- .../skills/api-discovery/agents/openai.yaml | 4 + .agents/skills/bump-gradle/SKILL.md | 6 +- .agents/skills/bump-version/SKILL.md | 10 +- .agents/skills/check-links/SKILL.md | 18 +- .agents/skills/check-links/agents/openai.yaml | 4 + .agents/skills/dependency-audit/SKILL.md | 10 +- .../dependency-audit/agents/openai.yaml | 4 + .agents/skills/dependency-update/SKILL.md | 4 +- .agents/skills/gradle-review/SKILL.md | 198 ++++++++ .../skills/gradle-review/agents/openai.yaml | 4 + .../skills/gradle-review/practices/README.md | 68 +++ .../skills/gradle-review/practices/tasks.md | 147 ++++++ .../gradle-review/spine-task-conventions.md | 81 ++++ .agents/skills/java-to-kotlin/SKILL.md | 2 +- .agents/skills/kotlin-engineer/SKILL.md | 65 +++ .../skills/kotlin-engineer/agents/openai.yaml | 4 + .../kotlin-engineer/references/build-setup.md | 92 ++++ .../kotlin-engineer/references/coroutines.md | 96 ++++ .../kotlin-engineer/references/idioms.md | 116 +++++ .agents/skills/kotlin-review/SKILL.md | 11 +- .../skills/kotlin-review/agents/openai.yaml | 4 + .agents/skills/move-files/SKILL.md | 2 +- .agents/skills/pre-pr/SKILL.md | 45 +- .agents/skills/pre-pr/agents/openai.yaml | 4 + .agents/skills/raise-coverage/SKILL.md | 241 ++++++++++ .../skills/raise-coverage/agents/openai.yaml | 4 + .../references/coverage-signals.md | 181 +++++++ .../references/migrate-to-kover.md | 352 ++++++++++++++ .agents/skills/review-docs/SKILL.md | 33 +- .agents/skills/review-docs/agents/openai.yaml | 4 + .agents/skills/update-copyright/SKILL.md | 14 +- .../update-copyright/agents/openai.yaml | 2 +- .agents/skills/version-bumped/SKILL.md | 22 +- .../skills/version-bumped/agents/openai.yaml | 4 + .../archive/raise-coverage-kover-migration.md | 449 ++++++++++++++++++ .agents/tasks/archive/raise-coverage.md | 283 +++++++++++ .../tasks/buildsrc-gradle-review-findings.md | 303 ++++++++++++ .../tasks/cross-agent-skill-best-practices.md | 100 ++++ .agents/tasks/enforce-max-line-length.md | 279 +++++++++++ .agents/tasks/gradle-caching-plan.md | 200 ++++++++ .agents/tasks/spine-task-group-constant.md | 104 ++++ .claude/commands/raise-coverage.md | 26 + AGENTS.md | 11 + buildSrc/build.gradle.kts | 4 +- buildSrc/src/main/kotlin/DokkaExts.kt | 5 +- .../src/main/kotlin/config-tester.gradle.kts | 5 +- .../io/spine/dependency/local/Validation.kt | 2 +- .../kotlin/io/spine/dependency/test/Jacoco.kt | 2 +- .../kotlin/io/spine/dependency/test/Kover.kt | 2 +- .../kotlin/io/spine/gradle/ConfigTester.kt | 8 +- .../kotlin/io/spine/gradle/SpineTaskGroup.kt | 49 ++ .../kotlin/io/spine/gradle/dart/task/Build.kt | 9 +- .../io/spine/gradle/dart/task/DartTasks.kt | 19 +- .../spine/gradle/dart/task/IntegrationTest.kt | 6 +- .../io/spine/gradle/dart/task/Publish.kt | 9 +- .../gradle/github/pages/UpdateGitHubPages.kt | 11 +- .../gradle/javadoc/ExcludeInternalDoclet.kt | 6 +- .../spine/gradle/javascript/task/Assemble.kt | 11 +- .../io/spine/gradle/javascript/task/Check.kt | 11 +- .../io/spine/gradle/javascript/task/Clean.kt | 7 +- .../gradle/javascript/task/IntegrationTest.kt | 7 +- .../spine/gradle/javascript/task/JsTasks.kt | 22 +- .../gradle/javascript/task/LicenseReport.kt | 5 +- .../spine/gradle/javascript/task/Publish.kt | 11 +- .../spine/gradle/javascript/task/Webpack.kt | 5 +- .../io/spine/gradle/publish/IncrementGuard.kt | 5 +- .../io/spine/gradle/publish/PublishingExts.kt | 26 +- .../gradle/report/coverage/CodebaseFilter.kt | 8 + .../gradle/report/coverage/FileExtension.kt | 8 + .../gradle/report/coverage/FileExtensions.kt | 37 +- .../gradle/report/coverage/FileFilter.kt | 10 +- .../gradle/report/coverage/JacocoConfig.kt | 18 +- .../gradle/report/coverage/KoverConfig.kt | 329 +++++++++++++ .../gradle/report/coverage/PathMarker.kt | 8 + .../spine/gradle/report/coverage/TaskName.kt | 11 +- .../gradle/report/license/LicenseReporter.kt | 3 + .../spine/gradle/report/pom/PomGenerator.kt | 3 + .../kotlin/io/spine/gradle/testing/Tasks.kt | 7 +- .../src/main/kotlin/jacoco-kmm-jvm.gradle.kts | 17 +- .../main/kotlin/jacoco-kotlin-jvm.gradle.kts | 15 +- .../src/main/kotlin/jvm-module.gradle.kts | 5 +- .../src/main/kotlin/kmp-module.gradle.kts | 2 +- .../main/kotlin/uber-jar-module.gradle.kts | 7 +- .../src/main/kotlin/write-manifest.gradle.kts | 6 +- config | 2 +- 88 files changed, 4228 insertions(+), 169 deletions(-) create mode 100644 .agents/skills/api-discovery/agents/openai.yaml create mode 100644 .agents/skills/check-links/agents/openai.yaml create mode 100644 .agents/skills/dependency-audit/agents/openai.yaml create mode 100644 .agents/skills/gradle-review/SKILL.md create mode 100644 .agents/skills/gradle-review/agents/openai.yaml create mode 100644 .agents/skills/gradle-review/practices/README.md create mode 100644 .agents/skills/gradle-review/practices/tasks.md create mode 100644 .agents/skills/gradle-review/spine-task-conventions.md create mode 100644 .agents/skills/kotlin-engineer/SKILL.md create mode 100644 .agents/skills/kotlin-engineer/agents/openai.yaml create mode 100644 .agents/skills/kotlin-engineer/references/build-setup.md create mode 100644 .agents/skills/kotlin-engineer/references/coroutines.md create mode 100644 .agents/skills/kotlin-engineer/references/idioms.md create mode 100644 .agents/skills/kotlin-review/agents/openai.yaml create mode 100644 .agents/skills/pre-pr/agents/openai.yaml create mode 100644 .agents/skills/raise-coverage/SKILL.md create mode 100644 .agents/skills/raise-coverage/agents/openai.yaml create mode 100644 .agents/skills/raise-coverage/references/coverage-signals.md create mode 100644 .agents/skills/raise-coverage/references/migrate-to-kover.md create mode 100644 .agents/skills/review-docs/agents/openai.yaml create mode 100644 .agents/skills/version-bumped/agents/openai.yaml create mode 100644 .agents/tasks/archive/raise-coverage-kover-migration.md create mode 100644 .agents/tasks/archive/raise-coverage.md create mode 100644 .agents/tasks/buildsrc-gradle-review-findings.md create mode 100644 .agents/tasks/cross-agent-skill-best-practices.md create mode 100644 .agents/tasks/enforce-max-line-length.md create mode 100644 .agents/tasks/gradle-caching-plan.md create mode 100644 .agents/tasks/spine-task-group-constant.md create mode 100644 .claude/commands/raise-coverage.md create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/SpineTaskGroup.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/KoverConfig.kt diff --git a/.agents/_TOC.md b/.agents/_TOC.md index 4be0656bdf..2f9ba238b2 100644 --- a/.agents/_TOC.md +++ b/.agents/_TOC.md @@ -21,3 +21,5 @@ 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) +22. [Gradle review](skills/gradle-review/SKILL.md) +23. [Raise test coverage](skills/raise-coverage/SKILL.md) diff --git a/.agents/documentation-guidelines.md b/.agents/documentation-guidelines.md index 58a64a396d..e034501e46 100644 --- a/.agents/documentation-guidelines.md +++ b/.agents/documentation-guidelines.md @@ -6,6 +6,27 @@ - When using TODO comments, follow the format on the [dedicated page][todo-comments]. - File and directory names should be formatted as code. +## API documentation scope + +KDoc and Javadoc describe the API as it appears to a consumer of the published +artifact. Keep them focused on behaviour, parameters, return values, and usage +examples. + +Do **not** reference repository-internal locations from API docs: + +- Build infrastructure paths such as `buildSrc/` or `config/` (the `config` + repository, `config/buildSrc/`, and similar). +- Agent-facing material under `.agents/` — task plans, skill rules, review + notes, conventions, or any other file rooted there. +- Branch names, commit SHAs, issue numbers, or other repo workflow artefacts. + +These details are invisible to a consumer who only sees the artifact's +sources/Javadoc/KDoc and rot quickly as the repository evolves. If the rationale +for an API decision lives in such a file, summarise the *outcome* in the +KDoc instead of linking to the source. Cross-repository parity notes and +work-in-progress justifications belong in the task plan under +`.agents/tasks/`, not in the published API documentation. + ## Protobuf file headers - In `.proto` files, a multi-paragraph documentation header must end with a trailing empty comment line (`//`). diff --git a/.agents/skills/api-discovery/SKILL.md b/.agents/skills/api-discovery/SKILL.md index b1622ffd10..e8a616e3a3 100644 --- a/.agents/skills/api-discovery/SKILL.md +++ b/.agents/skills/api-discovery/SKILL.md @@ -2,9 +2,9 @@ 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 + so you can inspect 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. --- @@ -12,7 +12,8 @@ description: > 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`. +straight to normal search and file-reading tools such as `rg`, `sed`, +or the active agent's file viewer. Do **not** run `find ~/.gradle/caches` or `unzip` against cache JARs. Each `unzip` decompresses the archive afresh — slow and token-heavy. @@ -42,7 +43,7 @@ the user should know about. | 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. | +| `0` | Path on stdout is usable. | Search or read files under that path directly. 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. | @@ -86,7 +87,7 @@ paths entirely. ## Workflow 1. **Always** call `discover` before reading library source. -2. Use the returned path with `Grep`/`Read`/`Glob` directly. Do **not** +2. Use the returned path with search or file-reading tools 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 @@ -197,11 +198,10 @@ $ echo $? 0 ``` -Tool calls then look like: +Follow-up searches then look like: -- `Glob` pattern `**/*.kt`, path - `/Users//Projects/Spine/base-libraries/base`. -- `Grep` pattern `class Identifier`, path the same. +- `rg --files /Users//Projects/Spine/base-libraries/base`. +- `rg -n 'class Identifier' /Users//Projects/Spine/base-libraries/base`. **Spine artifact, stale sibling:** diff --git a/.agents/skills/api-discovery/agents/openai.yaml b/.agents/skills/api-discovery/agents/openai.yaml new file mode 100644 index 0000000000..b274275cf9 --- /dev/null +++ b/.agents/skills/api-discovery/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "API Discovery" + short_description: "Resolve Maven artifact source paths" + default_prompt: "Use $api-discovery to resolve a Maven artifact's source path before inspecting library APIs or implementations." diff --git a/.agents/skills/bump-gradle/SKILL.md b/.agents/skills/bump-gradle/SKILL.md index 22f295786d..f229159d33 100644 --- a/.agents/skills/bump-gradle/SKILL.md +++ b/.agents/skills/bump-gradle/SKILL.md @@ -143,6 +143,6 @@ empty commits, and do not bundle unrelated changes into either commit. 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. + Run the `version-bumped` skill — it is a no-op if a bump has already + happened earlier on the branch, and otherwise uses the `bump-version` + skill to perform the increment. diff --git a/.agents/skills/bump-version/SKILL.md b/.agents/skills/bump-version/SKILL.md index 3e1d3d6598..8a882be885 100644 --- a/.agents/skills/bump-version/SKILL.md +++ b/.agents/skills/bump-version/SKILL.md @@ -24,7 +24,9 @@ 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. + 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*). @@ -81,6 +83,12 @@ create the commit. 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. diff --git a/.agents/skills/check-links/SKILL.md b/.agents/skills/check-links/SKILL.md index a4c61a0c5b..7c703be954 100644 --- a/.agents/skills/check-links/SKILL.md +++ b/.agents/skills/check-links/SKILL.md @@ -6,8 +6,10 @@ description: > 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). + the user asks to "check doc links". If no Hugo site exists under `docs/` or + `site/`, report the check as not applicable instead of failing. 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) @@ -48,11 +50,15 @@ both the skill and CI). `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`. +- The user asks to "check the doc links" or invokes the `check-links` skill. If none of the above is true, decline with a one-line note rather than running the (~30 s) build+check. +If the repository has no Hugo config under `docs/` or `site/`, return +`APPROVE — no Hugo documentation site found under docs/ or site/.` and stop. +Do not write a `FAIL` sentinel for this not-applicable case. + ## Tooling The skill needs four binaries: @@ -96,8 +102,8 @@ for dir in docs site; do done done if [ -z "$SITE_DIR" ]; then - echo "ERROR: No Hugo config found under docs/ or site/." >&2 - exit 1 + echo "APPROVE — no Hugo documentation site found under docs/ or site/." + exit 0 fi if [ -f "${SITE_DIR}/_preview/package-lock.json" ]; then @@ -194,7 +200,7 @@ 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 +The server must survive across multiple shell/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. diff --git a/.agents/skills/check-links/agents/openai.yaml b/.agents/skills/check-links/agents/openai.yaml new file mode 100644 index 0000000000..407bdae411 --- /dev/null +++ b/.agents/skills/check-links/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Check Links" + short_description: "Validate rendered Hugo documentation links" + default_prompt: "Use $check-links to build the Hugo docs site, run Lychee against the rendered HTML, and report broken links." diff --git a/.agents/skills/dependency-audit/SKILL.md b/.agents/skills/dependency-audit/SKILL.md index 010c16bced..af01f5d50b 100644 --- a/.agents/skills/dependency-audit/SKILL.md +++ b/.agents/skills/dependency-audit/SKILL.md @@ -47,12 +47,12 @@ Each file declares a Kotlin `object` extending `Dependency` or `DependencyWithBo 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 +2. **Lean on the diff; read files 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` + read individual files — work from the diff and use targeted `rg` for cross-cutting questions. 3. **Batch independent work into one turn.** Issue the version-sanity (A), @@ -75,7 +75,7 @@ Each file declares a Kotlin `object` extending `Dependency` or `DependencyWithBo 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` + 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. @@ -116,7 +116,7 @@ When an artifact is **renamed or removed**: ### 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). + (the user can run the `update-copyright` skill to fix). - **GitHub URL comment.** New `lib/` and `kotlinx/` files conventionally start with `// https://github.com//` above the object. Recommend it if missing. diff --git a/.agents/skills/dependency-audit/agents/openai.yaml b/.agents/skills/dependency-audit/agents/openai.yaml new file mode 100644 index 0000000000..c3758f31ef --- /dev/null +++ b/.agents/skills/dependency-audit/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Dependency Audit" + short_description: "Review dependency declaration diffs" + default_prompt: "Use $dependency-audit to review dependency declaration changes for version sanity, BOM consistency, deprecations, and convention drift." diff --git a/.agents/skills/dependency-update/SKILL.md b/.agents/skills/dependency-update/SKILL.md index 7e70bc126c..c9ddee4209 100644 --- a/.agents/skills/dependency-update/SKILL.md +++ b/.agents/skills/dependency-update/SKILL.md @@ -242,11 +242,11 @@ When the run completes, emit a Markdown report with these sections: 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 +2. Run the `version-bumped` skill. 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. + and otherwise uses the `bump-version` skill 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: diff --git a/.agents/skills/gradle-review/SKILL.md b/.agents/skills/gradle-review/SKILL.md new file mode 100644 index 0000000000..1d4ada40e3 --- /dev/null +++ b/.agents/skills/gradle-review/SKILL.md @@ -0,0 +1,198 @@ +--- +name: gradle-review +description: > + Review Gradle-related changes in this repo against Spine SDK conventions + and the upstream Gradle best-practices guides ingested under `practices/`. + Three scopes: (1) `buildSrc/` in the `config` repository only; + (2) Gradle build files in any project; (3) production code of Gradle + plugins exposed by Spine SDK tools. Use after any non-trivial change to + build logic, before opening a PR, or when asked for a Gradle review. + Read-only; does not run builds. +--- + +# Gradle review (repo-specific) + +You are the Gradle reviewer for a Spine Event Engine project. You review +Gradle build logic and plugin production code; you do **not** duplicate +`kotlin-review` (Kotlin idioms, safety rules, tests, version-gate) or +`dependency-audit` (artifact declarations under +`buildSrc/src/main/kotlin/io/spine/dependency/`). + +The authoritative standards live in two places: + +- **Spine-specific Gradle rules** — + [`spine-task-conventions.md`](spine-task-conventions.md) in this + skill directory. Documents the `group = "spine"` mandate and the + `description` requirement on every custom task. +- **Upstream Gradle best practices** — `practices/` in this skill + directory. One Markdown file per ingested Gradle docs page; each file + links back to the source URL and pins the Gradle version it was derived + from. The initial ingest is the "Tasks" best-practices page; more + pages are added over time. See `practices/README.md` for the ingest + procedure. + +## Scope + +This skill reviews three classes of files: + +1. **`buildSrc/` in the `config` repository only.** Detect via + + git remote -v + + The repo whose *any* remote URL matches the regex + `[:/]SpineEventEngine/config(\.git)?$` is `config`. The character + class `[:/]` covers both forms — ssh + (`git@github.com:SpineEventEngine/config.git`) and https + (`https://github.com/SpineEventEngine/config.git`) — and scanning + every remote (not just `origin`) handles forks where `origin` + points at a personal mirror and `upstream` points at the canonical + remote. + + In any other repo, treat `buildSrc/` as local scaffolding owned by + the consuming project and skip its files — *except* + `buildSrc/src/main/kotlin/module.gradle.kts`, which `AGENTS.md § + Code review` carves out as consumer-owned and therefore in scope. + +2. **Gradle build files of the current project.** Anywhere: + + - `**/build.gradle.kts`, `**/settings.gradle.kts` + - `**/*.gradle.kts` precompiled scripts outside `buildSrc/` + (in `config`, precompiled scripts inside `buildSrc/` fall under + scope 1 instead) + +3. **Production code of Gradle plugins exposed by Spine SDK tools.** + Files under `src/main/kotlin/` or `src/main/java/` that are part of a + Gradle plugin. Detect by any of: + + - Class implements `org.gradle.api.Plugin` or + `org.gradle.api.Plugin`. + - Class extends `org.gradle.api.DefaultTask`, + `org.gradle.api.tasks.SourceTask`, `JavaExec`, `Exec`, `Copy`, etc. + - The owning module declares a `gradlePlugin { plugins { ... } }` + block in its `build.gradle.kts`, or ships a + `META-INF/gradle-plugins/*.properties` resource. + +If after filtering nothing in the diff falls in any scope, return +`APPROVE — no Gradle-related changes.` and stop. + +## Review procedure + +1. **Scope the diff.** Obtain the change set via `git diff --staged` or + `git diff ...HEAD` depending on what the user describes + (default ` = origin/master`). Apply the scope rules above. + Then filter file paths against `AGENTS.md § Code review`: + - In **`config` itself** only `gradlew` and `gradlew.bat` are + skipped — every other config-distributed path is owned by this + repo and stays in scope. + - In any **consumer repo**, honour the full config-distributed + skip list (with the `module.gradle.kts` carve-out from scope 1). + If filtering leaves the set empty in a consumer repo, return + `APPROVE — all changes are config-distributed files.` and stop. + +2. **Read each affected file fully**, not just the hunks. Task + registration blocks span multiple lines; lazy-config and + cache-correctness issues only become visible with surrounding + context (e.g., a `Provider.get()` six lines above a + `tasks.register {}` call). + +3. **Check Spine-specific rules** (from + [`spine-task-conventions.md`](spine-task-conventions.md)): + + - Every custom task registered or configured in scope sets both + `group` and `description`. + - `group` equals `"spine"`. Once the shared constant exists (see + [`.agents/tasks/spine-task-group-constant.md`](../../tasks/spine-task-group-constant.md)), + a bare literal `"spine"` where the constant could have been used + becomes a Nit whose recommended replacement is the constant. + +4. **Check upstream Gradle best practices** (from `practices/`): + + - **Tasks** ([`practices/tasks.md`](practices/tasks.md), derived + from the Gradle Tasks best-practices page[^gradle-tasks]): + `dependsOn` vs. input/output wiring, cacheability annotations, + no `Provider.get()` in configuration outside an action, no eager + `FileCollection` / `Configuration` APIs, no early configuration + resolution, correct `@PathSensitivity`, unique outputs. + - Any additional `practices/*.md` files ingested since this skill + was written. Treat + [`practices/README.md`](practices/README.md)'s table as the + authoritative list of ingested pages. + +5. **Batch independent checks.** Issue the most common ripgrep recipes + in parallel within a single response — examples: + + - `rg -n 'tasks\.create\(' --type kotlin` + — eager registration (`--type kotlin` is ripgrep's built-in + type that covers both `*.kt` and `*.kts`; the short alias + `--type kt` is **not** recognised). + - `rg -n '\.files\b|\.getFiles\b|\.size\b|\.isEmpty\b|\.toList\b|\.asPath\b' --glob '*.gradle.kts' --glob '*.kt' --glob '*.java'` + — eager file-collection APIs (covers Kotlin property access, + method invocation, and the Java `getFiles()` accessor in plugin + production code). + - `rg -n 'group\s*=\s*"spine"' --glob '*.gradle.kts' --glob '*.kt'` + — confirm the Spine group is used; the absence in a `register` + block is the finding. + - `rg -n '@CacheableTask|@DisableCachingByDefault' --type kotlin` + — locate plugin task classes that should carry an annotation. + + Collect every finding and emit the report once — **do not stop at + the first failure**. + +## Output format + +Three sections, in this order, matching `kotlin-review`, +`review-docs`, and `dependency-audit`: + +- **Must fix** — Spine mandate violations (missing `group` or + `description`; `group` not equal to `"spine"`); upstream + correctness-breaking patterns (`Provider.get()` outside a task + action; `Configuration` resolved during configuration; eager + `FileCollection` / `Configuration` APIs that discard implicit task + dependencies; overlapping task outputs); mixing Groovy and Kotlin + DSL in build logic. +- **Should fix** — upstream Gradle recommendations whose failure mode + is cache-miss performance or idiomatic concern: `dependsOn` where + input/output wiring would express the link; missing `@CacheableTask` + / `@DisableCachingByDefault` on a plugin task class; missing or + wrong `@PathSensitivity`; `tasks.create(...)` instead of + `tasks.register(...)`. +- **Nits** — task name not action-oriented camelCase; `description` + not in the imperative form documented by + [`spine-task-conventions.md`](spine-task-conventions.md); + the literal `"spine"` written where the shared constant exists; + missing KDoc back-link to the Gradle docs anchor that motivated a + rule. + +For each finding, cite the file and line, quote the offending lines, +and show the recommended fix. If a section is empty, write "None." + +End with a one-line verdict: `APPROVE`, `APPROVE WITH CHANGES`, or +`REQUEST CHANGES`. + +## Extending this skill + +This skill is self-extensible. Two triggers, both **user-initiated**: + +1. **Gradle release.** When the project upgrades the Gradle wrapper + (`gradle/wrapper/gradle-wrapper.properties`), reread each + `practices/*.md` against the matching + `docs.gradle.org//userguide/...` page and refresh content + that has changed. Bump the `gradle-version` and `ingested` fields + and the table in `practices/README.md`. + +2. **New page or rule.** When a maintainer asks to add a practice from + another Gradle docs page (or a new Spine rule), follow + `practices/README.md`: + + 1. Fetch the target Gradle docs page. + 2. Add a new Markdown file under `practices/` (slug from the page + anchor). + 3. Update the table in `practices/README.md`. + 4. Update this `SKILL.md`'s "Check upstream Gradle best practices" + list if the new page introduces categories the procedure did + not enumerate before. + +The skill never auto-fetches. The user runs the `gradle-review` skill for a +review, and explicitly asks for an ingest/refresh when one is wanted. + +[^gradle-tasks]: https://docs.gradle.org/9.5.1/userguide/best_practices_tasks.html diff --git a/.agents/skills/gradle-review/agents/openai.yaml b/.agents/skills/gradle-review/agents/openai.yaml new file mode 100644 index 0000000000..5fc2e6097f --- /dev/null +++ b/.agents/skills/gradle-review/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Gradle Review" + short_description: "Review Gradle build logic changes" + default_prompt: "Use $gradle-review to review Gradle build logic and plugin changes against Spine conventions and ingested Gradle best practices." diff --git a/.agents/skills/gradle-review/practices/README.md b/.agents/skills/gradle-review/practices/README.md new file mode 100644 index 0000000000..ac92bd3588 --- /dev/null +++ b/.agents/skills/gradle-review/practices/README.md @@ -0,0 +1,68 @@ +# Gradle best-practices index + +This directory mirrors selected pages of the upstream Gradle "Best +practices" user guide. Each file is derived from one Gradle docs page +and links back to its source URL. The `gradle-review` skill references +these files when reviewing changes. + +## Gradle version pin + +The notes here track Gradle **9.5.1** — the version pinned by +`gradle/wrapper/gradle-wrapper.properties` in this repository at the +time of ingest. When the wrapper is bumped, refresh each `*.md` below +against the matching `docs.gradle.org//userguide/...` page and +update this section. + +## Ingested pages + +| File | Source | Last reviewed | +|------|--------|---------------| +| [tasks.md](tasks.md) | | 2026-05-29 | + +## Ingest procedure + +Ingests are **user-initiated only.** This procedure runs when a +maintainer explicitly asks for a new practice page or for a refresh +(typically after a Gradle wrapper bump). The skill never auto-fetches +Gradle docs. + +1. Identify the Gradle docs page URL. +2. Pick a slug from the page's anchor (e.g. `tasks`, `dependencies`, + `configurations`). Keep slugs short and kebab-case. +3. Create `practices/.md` with this frontmatter: + + --- + source: + gradle-version: + ingested: + --- + +4. For each best practice on the page, write a short section with: + - **The rule.** One sentence. + - **Why it matters.** One sentence — the rationale Gradle cites. + - **Spine review level.** One of `Must fix`, `Should fix`, `Nit`. + Map upstream "recommended" items by the failure mode they + prevent: build-correctness failures or lost task dependencies → + `Must fix`; cache-miss performance and idiomatic concerns → + `Should fix`; style and naming → `Nit`. + +5. If the page introduces a category not covered by the current + `SKILL.md` "Check upstream Gradle best practices" list, edit that + list. + +6. Add a row to the table above. Bump the `Last reviewed` date. + +## Spine additions + +Some `gradle-review` checks have no direct upstream counterpart but +follow from existing Spine guidelines: + +- **`tasks.create(...)` vs. `tasks.register(...)`** — Spine prefers + lazy registration. The rule cross-references the `@since 4.9` + Gradle documentation on lazy configuration but is enforced as a + Spine review item. +- **Mixing Groovy and Kotlin DSL** — Spine projects use Kotlin DSL + exclusively (`*.gradle.kts`, `*.kt`). + +These are documented inside the relevant `practices/*.md` "Spine +additions" sections so reviewers see them alongside the upstream rules. diff --git a/.agents/skills/gradle-review/practices/tasks.md b/.agents/skills/gradle-review/practices/tasks.md new file mode 100644 index 0000000000..f1536b59e1 --- /dev/null +++ b/.agents/skills/gradle-review/practices/tasks.md @@ -0,0 +1,147 @@ +--- +source: https://docs.gradle.org/9.5.1/userguide/best_practices_tasks.html +gradle-version: 9.5.1 +ingested: 2026-05-29 +--- + +# Tasks — Gradle best practices + +Source: the Gradle "Best practices for tasks" user-guide page[^src]. + +The Gradle user guide enumerates a set of best practices for tasks. +Each is mapped below to a Spine review level used by the +`gradle-review` skill. + +## Spine-specific must-fix + +From [`spine-task-conventions.md`](../spine-task-conventions.md): + +- Every custom task must set `group`. The value must equal `"spine"` + (use the shared constant once introduced — see + [`.agents/tasks/spine-task-group-constant.md`](../../../tasks/spine-task-group-constant.md)). +- Every custom task must set `description`. + +These are **Must fix** findings in `gradle-review`. + +## Upstream practices + +### 1. Avoid `dependsOn` — *Should fix* + +Use input/output wiring (`Provider`-typed `inputs`/`outputs` and +producer-task references) instead of explicit `dependsOn(...)` for the +*action* graph. Wiring tells Gradle *why* one task needs another, +which in turn enables incremental builds and accurate task selection. + +`dependsOn` remains correct for lifecycle tasks — tasks without task +actions — per the upstream guidance. (Finalizer relations are wired +with `finalizedBy(...)`, not `dependsOn(...)`.) + +### 2. Favor `@CacheableTask` / `@DisableCachingByDefault` — *Should fix* + +Annotate task classes for cacheability instead of calling +`outputs.cacheIf {}` at registration time. The annotation documents +the contract in source and avoids re-evaluating the predicate on +every configuration. + +### 3. Don't call `get()` on a `Provider` outside a task action — *Must fix* + +`Provider.get()` during configuration forces immediate evaluation, +breaks the configuration cache, and serialises work that Gradle would +otherwise run in parallel. Compose providers with `map(...)` / +`flatMap(...)` and defer `get()` to the `@TaskAction` method. + +### 4. Group and describe custom tasks — *Must fix* + +Set `group` and `description` on every custom task. Tasks without a +group are hidden from `./gradlew tasks` unless `--all` is passed. +They are also excluded from the default IntelliJ IDEA Gradle +tool-window listing (Spine addendum from +[`spine-task-conventions.md`](../spine-task-conventions.md)). + +**Spine addendum:** `group` must equal `"spine"`. + +### 5. Avoid eager APIs on `FileCollection` / `Configuration` — *Must fix* + +`.size()`, `.isEmpty()`, `.files` / `getFiles()`, `asPath()`, and +`.toList()` on a `Configuration` or `FileCollection` trigger +dependency resolution during the configuration phase **and discard +any implicit task dependencies the collection carried** — the latter +is a wrong-outputs failure mode, not a performance one. Consume the +collection lazily via `@InputFiles` / `@Classpath` and +`Provider<...>` chains. + +### 6. Don't resolve `Configuration`s before task execution — *Must fix* + +Resolving a `Configuration` during configuration (e.g., calling +`configuration.resolve()`, `configuration.resolvedConfiguration`, or +reading `.files` from one) loses task-dependency tracking and slows +unrelated tasks because every build path triggers resolution. Resolve +inside the `@TaskAction` only. + +### 7. Use the right `@PathSensitivity` — *Should fix* + +Pick the sensitivity that matches what the task's output actually +depends on: + +- **`@PathSensitivity.NONE`** — content-only inputs where the file + name and location do not affect outputs: classpath JAR entries, + binary blobs, signed/checksummed bundles, etc. +- **`@PathSensitivity.RELATIVE`** — inputs whose relative path is + part of the task's contract: source-tree files such as `.proto`, + `.kt`, `.java`, or templated resources, where the relative path + encodes the package/module/output location. +- **`@PathSensitivity.NAME_ONLY`** — when only the file name (not + the directory) matters; rare but applicable to per-name lookup + tables and similar. +- **`@PathSensitivity.ABSOLUTE`** — almost never correct; defeats + cache portability and should appear with a justifying comment. + +Mismatches show up as cache misses (over-strict sensitivity) or +incorrect cache hits (under-strict sensitivity — the more dangerous +direction). Annotating proto-compilation source inputs with `NONE`, +for example, will cause incremental builds to miss renames that +change package structure. + +### 8. Use unique output files and directories — *Must fix* + +Two tasks must not write to overlapping outputs (either inside one +project or across projects). Overlap causes unnecessary reruns, can +mask stale outputs, and may corrupt incremental builds. Each task +writes to its own deterministic location, typically under +`layout.buildDirectory.dir("…")`. + +## Spine additions (not on the upstream page) + +- **`tasks.create(...)` vs. `tasks.register(...)` — *Should fix*.** + `register` is lazy and aligns with every other recommendation on + this page. New code should always use `register`. Configuring an + existing task with `tasks.named(...)` is also lazy and preferred + over `tasks.getByName(...)`. + +- **Mixing Groovy and Kotlin DSL — *Must fix*.** Spine projects use + Kotlin DSL exclusively (`*.gradle.kts`, `*.kt`). Catch any + `.gradle` Groovy script slipping into `buildSrc/` or the project + root. + +## Nits + +- **Task names** should be action-oriented camelCase + (`generateSpineModel`, not `spine_model_generator` or + `spineModelGen`). +- **`description`** should read as an imperative sentence + (`"Generates Spine model classes from .proto definitions"`). + [`spine-task-conventions.md`](../spine-task-conventions.md) is the + canonical source; this Nit tracks whatever convention that file + establishes. +- **`"spine"` as a string literal.** Once the shared constant exists + (see + [`.agents/tasks/spine-task-group-constant.md`](../../../tasks/spine-task-group-constant.md)), + the literal `"spine"` in `buildSrc/` code, build files, or plugin + production code is a Nit unless wrapped in a comment with a TODO + referencing the migration. +- **KDoc back-link.** A public custom task class should link the + Gradle docs anchor that motivated its design (the relevant rule + above in this file, or the upstream page[^src]) so future readers + know which best practice the class implements. + +[^src]: https://docs.gradle.org/9.5.1/userguide/best_practices_tasks.html diff --git a/.agents/skills/gradle-review/spine-task-conventions.md b/.agents/skills/gradle-review/spine-task-conventions.md new file mode 100644 index 0000000000..a8278c0d6f --- /dev/null +++ b/.agents/skills/gradle-review/spine-task-conventions.md @@ -0,0 +1,81 @@ +# Spine task conventions + +This file is the authoritative source for Spine SDK rules on Gradle +custom tasks. The `gradle-review` skill enforces them, and +`practices/tasks.md` cross-references the rule alongside the upstream +Gradle "Best practices for tasks" page. + +## Background: `group` and `description` are metadata + +The `group` and `description` properties on a Gradle `Task` are +**metadata only**. They control how tasks are organised and displayed +in: + +- `./gradlew tasks` +- The IntelliJ IDEA Gradle tool window +- Other build tools + +They have **no impact** on task execution or task-dependency wiring. + +Gradle and the Kotlin Gradle plugin intentionally place core tasks +(`compileJava`, `compileKotlin`, `processResources`, …) into the +**`other`** group to keep the default task list clean. High-level +tasks use the conventional groups `build`, `verification`, +`documentation`, and `publishing`. + +## Rule + +Every custom task registered or configured by Spine SDK code must set +both: + +- **`group`** equal to the string `"spine"`. Use the shared constant + once it exists — see + [`../../tasks/spine-task-group-constant.md`](../../tasks/spine-task-group-constant.md). +- **`description`** as a short imperative sentence describing what + the task does (no trailing period). + +The rule applies to: + +- `tasks.register(...) { … }` and `tasks.create(...) { … }`. +- `tasks.withType<…>().configureEach { … }`. +- Plugin production code that programmatically registers or + configures tasks (`Plugin` implementations under + `tool-base` and similar repos). + +Both examples below reference the shared constant +`io.spine.gradle.SpineTaskGroup.name`, which holds the value +`"spine"` and is visible to every `build.gradle.kts` because it +lives in `buildSrc/`. + +### Example — registering a new task + +```kotlin +import io.spine.gradle.SpineTaskGroup + +tasks.register("generateSpineModel") { + group = SpineTaskGroup.name + description = "Generates Spine model classes from .proto definitions" + // ... +} +``` + +### Example — configuring an existing task type + +```kotlin +import io.spine.gradle.SpineTaskGroup + +tasks.withType().configureEach { + group = SpineTaskGroup.name + description = "Compiles Spine-specific module sources" +} +``` + +## Why this matters + +- Makes Spine-specific tasks easy to discover in the IDE and on the + command line, especially in large multi-plugin projects. +- Mirrors the convention established by Dokka, Ktlint, Shadow, and + similar third-party plugins — each places its tasks in a single + named group. +- Lets the `gradle-review` skill cross-check task registration code + against one consistent rule. diff --git a/.agents/skills/java-to-kotlin/SKILL.md b/.agents/skills/java-to-kotlin/SKILL.md index b9835f8f7a..7b603ab5b2 100644 --- a/.agents/skills/java-to-kotlin/SKILL.md +++ b/.agents/skills/java-to-kotlin/SKILL.md @@ -52,7 +52,7 @@ description: > ## Final step: ensure the version is bumped -After the conversion is verified, invoke `/version-bumped` so the branch +After the conversion is verified, run the `version-bumped` skill 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). diff --git a/.agents/skills/kotlin-engineer/SKILL.md b/.agents/skills/kotlin-engineer/SKILL.md new file mode 100644 index 0000000000..aa0d6ca56e --- /dev/null +++ b/.agents/skills/kotlin-engineer/SKILL.md @@ -0,0 +1,65 @@ +--- +name: kotlin-engineer +description: > + Kotlin 2.x policy and pitfalls. Use when writing, reviewing, or refactoring + Kotlin code — enforces coroutine-safety, Flow correctness, null-safety, and + API-design rules that LLMs frequently get wrong. +--- + +# Kotlin — policy & pitfalls + +Baseline Kotlin knowledge (data/sealed/value classes, scope functions, null-safety operators, extension functions, `suspend`, `Flow`, `when` exhaustiveness) is assumed. This skill does not teach the language — it encodes the project policy and the traps that keep appearing in code review. + +## Setup Check (run first) + +Before writing non-trivial code: + +1. **Kotlin version** — target 2.x when possible. Check `build.gradle(.kts)` (`kotlin("jvm") version "2.x"`) or `libs.versions.toml`. +2. **JDK target** — `kotlin { jvmToolchain(21) }` or `compileOptions { targetCompatibility = JavaVersion.VERSION_21 }`. Matters for virtual threads (21+) and records interop (17+). +3. **Compiler plugins** — `kotlin("plugin.spring")`, `kotlin("plugin.jpa")`, `kotlinx-serialization`, `kotlin("kapt")` vs `com.google.devtools.ksp`. Missing `plugin.spring` → final Spring classes can't be proxied. Missing `plugin.jpa` → `InstantiationException: No default constructor`. +4. **Lint** — `detekt` / `ktlint` configured? Follow the existing rules; don't introduce new violations. +5. **Build wrapper** — use `./gradlew` + +## MUST DO + +- **Null-safety via `?`, `?.`, `?:`, `let`, `requireNotNull`.** Use `!!` only when null is a true contract violation — document why on the same line. +- **Sealed hierarchies** for closed result / state types (`sealed interface Result { data class Success(...); data class Failure(...) }`) + exhaustive `when` without `else`. +- **Value classes (`@JvmInline value class`)** for domain identifiers (`UserId`, `Email`) — zero-overhead type-safety. +- **`data class` only for pure value types.** Not for entities, services, or anything with behavior / lifecycle. +- **Structured concurrency** — inject `CoroutineScope`, use `coroutineScope { }` / framework scopes (`viewModelScope`). Never `GlobalScope.launch`. +- **Always rethrow `CancellationException`** in generic `catch (e: Exception)` blocks — swallowing it disables cancellation. +- **Expose read-only Flow types** — `val state: StateFlow = _state.asStateFlow()`. Never leak `MutableStateFlow` / `MutableSharedFlow` from an API. +- **`withContext(Dispatchers.IO)` / `Default`** for blocking / CPU work inside suspend. Encapsulate dispatcher choice in the repository / data-source layer — not at call sites. +- **Immutability by default** — `val` over `var`, `List` over `MutableList` in public API, `copy()` on data classes instead of mutation. +- **Named arguments for 3+ parameters** — prevents silent argument swaps at call sites. + +## MUST NOT DO + +- **No `!!`** without a commented reason. Refactor to `?.let { }` / `requireNotNull(x) { "why" }`. +- **No `runBlocking` in production** — only in `main` and tests. Inside a suspend function it's always a bug. +- **No `GlobalScope.launch` / `GlobalScope.async`** — leaks, no structured cancellation. +- **No swallowing `CancellationException`.** `try/catch(Exception)` without a cancellation rethrow silently disables cancellation. +- **No `.first()` / `.single()` on a hot `Flow`** without a timeout — a source that never emits hangs the coroutine forever. +- **No `async { }.await()` sequentially** when you want parallelism — it's the same as calling `suspend` directly. Use `coroutineScope { val a = async { .. }; val b = async { .. }; a.await() + b.await() }`. +- **No `Dispatchers.Main` / `Dispatchers.IO` references from common / multiplatform code** unless the module is JVM-only. +- **No platform-type leaks (`String!`)** in public API — annotate Java interop returns with `@NotNull` / `@Nullable` on the Java side, or cast explicitly. +- **No catching `Throwable`** — you'll catch `OutOfMemoryError`, `StackOverflowError`, and cancellation. Use `Exception` and rethrow cancellation. +- **No `lateinit var` on primitives or nullable types** — compile error. Use `Delegates.notNull()` for primitives. + +## Reference Guide + +| Load when | File | +|---|---| +| Async / reactive code — coroutines, Flow, StateFlow/SharedFlow, cancellation, testing | `references/coroutines.md` | +| API design — scope functions, value/data/sealed classes, extension functions, inline/reified, delegates, `Result` | `references/idioms.md` | +| Gradle / tooling — Kotlin DSL, version catalogs, KSP vs kapt, multi-module layout, compiler plugins | `references/build-setup.md` | + +## Output Format + +When producing code: + +1. A short plan (1–3 bullets) of what's changing. +2. The code. +3. A checklist of the non-obvious MUST rules applied. + +When reviewing code: call out MUST-DO / MUST-NOT violations explicitly and suggest the minimal fix. diff --git a/.agents/skills/kotlin-engineer/agents/openai.yaml b/.agents/skills/kotlin-engineer/agents/openai.yaml new file mode 100644 index 0000000000..ed64b4efe2 --- /dev/null +++ b/.agents/skills/kotlin-engineer/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Kotlin Engineer" + short_description: "Kotlin 2.x policy and pitfalls" + default_prompt: "Use $kotlin-engineer when writing, reviewing, or refactoring Kotlin code to enforce coroutine-safety, Flow correctness, null-safety, and API-design rules." diff --git a/.agents/skills/kotlin-engineer/references/build-setup.md b/.agents/skills/kotlin-engineer/references/build-setup.md new file mode 100644 index 0000000000..0097d1fce6 --- /dev/null +++ b/.agents/skills/kotlin-engineer/references/build-setup.md @@ -0,0 +1,92 @@ +# Kotlin build setup — policy & pitfalls + +Assumes basic Gradle knowledge. This file is the setup policy and the compiler-plugin traps. + +## Build script style + +- **Kotlin DSL (`build.gradle.kts`)** over Groovy. Type-safe, better IDE support, same syntax as the rest of the codebase. +- **Version catalogs (`gradle/libs.versions.toml`)** for dependency versions. Don't hard-code versions in module scripts. + ```toml + [versions] + # Check latest stable at: https://kotlinlang.org/docs/releases.html + kotlin = "" + # Check latest stable at: https://github.com/Kotlin/kotlinx.coroutines/releases + coroutines = "" + [libraries] + coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } + [plugins] + kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } + ``` +- **`./gradlew`** always. If the wrapper is missing, generate it (`gradle wrapper --gradle-version X`) before anything else. + +## Kotlin compiler plugins — which are mandatory, which break builds if missing + +| Plugin | Needed when | Failure mode if missing | +|---|---|---| +| `kotlin("plugin.spring")` | Any Spring project | Final classes can't be CGLIB-proxied → `@Transactional` / `@Async` silently don't work | +| `kotlin("plugin.jpa")` | JPA entities | `InstantiationException: No default constructor for entity` at runtime | +| `kotlinx-serialization` | `@Serializable` data classes | `SerializationException: Serializer for class not found` | +| `kotlin("kapt")` / KSP | Annotation processors (Dagger, Room, Moshi codegen) | Processor simply doesn't run | +| `org.jlleitschuh.gradle.ktlint` / `io.gitlab.arturbosch.detekt` | If the project uses them | Lint noise in PRs | + +- `all-open` plugin is the underlying mechanism of `plugin.spring` / `plugin.jpa`. Don't apply `all-open` directly unless you know exactly which annotations to mark open. +- `no-arg` plugin generates a synthetic no-arg constructor for annotated classes — that's how `plugin.jpa` works. + +## KSP vs kapt + +- **Prefer KSP (`com.google.devtools.ksp`)** — significantly faster than kapt (2–7× depending on the processor), avoids the stub generation round-trip. +- **Fall back to kapt only when** the processor doesn't have a KSP implementation (still a few legacy ones). Migrate when possible: Room, Moshi, Hilt all have KSP now. +- Don't mix KSP and kapt for the same processor in the same module — conflicts, build slowdown. +- kapt pitfall: `kapt` tasks ignore incremental compilation by default. Large multi-module projects waste minutes per build. + +## JVM toolchain + +- **Set `jvmToolchain(n)`** — Gradle downloads the right JDK if missing: + ```kotlin + kotlin { + jvmToolchain(21) + } + ``` +- This replaces `sourceCompatibility` / `targetCompatibility` / `kotlinOptions.jvmTarget`. Setting all three is redundant and error-prone (they drift). +- Virtual threads require JDK 21+. Records interop requires JDK 17+. + +## `kotlinOptions` / `compilerOptions` + +- In Kotlin 2.x use the DSL: + ```kotlin + kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_21 + freeCompilerArgs.addAll( + "-Xjsr305=strict", // treat JSR-305 annotations as strict + "-Xcontext-receivers", // if you use context receivers + ) + } + } + ``` +- `-Xjsr305=strict` is strongly recommended when consuming Java APIs with `@Nullable` / `@NotNull` — turns platform types into proper Kotlin nullable types. +- Don't enable `-Werror` in library modules without a plan — breaks CI on benign warnings (deprecations you don't control). + +## Multi-module layout + +- **One-way dependencies.** `app` → `feature-*` → `core`. No `core` → `feature`. +- **Public API module boundary** — use `api(...)` in Gradle only when consumers need the type transitively; otherwise `implementation(...)`. `api` leaks the dependency and slows up compilation. +- Convention plugins (`buildSrc` or composite build `build-logic`) for shared config — don't duplicate `compilerOptions` / plugin ids across modules. + +## Common failure modes + +| Symptom | Cause | Fix | +|---|---|---| +| `@Transactional` silently doesn't start a transaction in Kotlin code | Class is `final` (Kotlin default) | Add `kotlin("plugin.spring")` — marks annotated classes `open` | +| `InstantiationException: No default constructor` on JPA entity | Kotlin class has no no-arg constructor | Add `kotlin("plugin.jpa")` | +| `SerializationException: Serializer for class X not found` | Missing `kotlinx-serialization` plugin or `@Serializable` on the class | Apply the plugin + annotation | +| `Duplicate class` from two versions of the same library | Transitive dependency conflict | `./gradlew :app:dependencies` → align via `constraints` block or version catalog | +| KSP / kapt not running | Plugin not applied OR task excluded in CI | Check `./gradlew :module:kspKotlin` runs standalone | +| Build suddenly slow after adding a library | kapt pulled in transitively | Check `./gradlew :module:dependencies` for `kapt` configuration; migrate to KSP | + +## What NOT to put in a Kotlin build + +- **Global `allprojects { kotlinOptions { ... } }`** — runs for every module, including those without Kotlin plugin applied → errors. Use a convention plugin. +- **`kotlin("jvm") version "..."` on subprojects** — declare the plugin at the root `plugins { }` block with `apply false`, then `plugins { alias(libs.plugins.kotlin.jvm) }` per module. +- **Absolute paths** in `gradle.properties` or scripts — breaks CI. Use project-relative paths. +- **`mavenLocal()`** as the first repository in CI — non-reproducible builds. Only for local debugging of a library you're developing. diff --git a/.agents/skills/kotlin-engineer/references/coroutines.md b/.agents/skills/kotlin-engineer/references/coroutines.md new file mode 100644 index 0000000000..df1130e7d4 --- /dev/null +++ b/.agents/skills/kotlin-engineer/references/coroutines.md @@ -0,0 +1,96 @@ +# Coroutines & Flow — policy & pitfalls + +Assumes you know `suspend`, `launch` / `async`, `Flow` / `StateFlow` / `SharedFlow`, `withContext`, `Dispatchers`. This file is only the traps and the rules. + +## Scopes & lifecycles + +- **Every coroutine must have an owner.** Framework-provided (`viewModelScope`, `lifecycleScope`, Ktor `call`), or a custom `CoroutineScope(SupervisorJob() + Dispatchers.Default + CoroutineName("service-x"))` held by the service. +- **`SupervisorJob` for long-lived services** (one child failing shouldn't kill siblings). **Regular `Job` inside a use case** — when one subtask fails, cancel the rest. +- `CoroutineScope(...)` as a local is almost always a bug — it doesn't cancel when its "parent" function returns. Use `coroutineScope { }` / `supervisorScope { }` instead, which are structured. +- Never store a `CoroutineScope` as a top-level / object property — that's `GlobalScope` with extra steps. + +## Cancellation + +- `CancellationException` is a **normal control-flow signal**, not an error. Always rethrow from generic catches: + ```kotlin + try { work() } catch (e: Exception) { + if (e is CancellationException) throw e + log.error("work failed", e) + } + ``` +- Non-suspending loops don't check for cancellation. Insert `ensureActive()` / `yield()` inside CPU-heavy loops that you want to cancel promptly. +- `NonCancellable` block is for cleanup (`withContext(NonCancellable) { close() }`) — never wrap business logic in it. +- `finally` after a `withTimeout` runs on a cancelled coroutine — don't call suspend functions there without `withContext(NonCancellable)` wrapping. + +## Dispatcher discipline + +- `Dispatchers.Main` — UI callbacks only. +- `Dispatchers.IO` — blocking I/O (JDBC, `File`, `Socket`). Pool size is `max(64, availableProcessors)` by default (so on a 128-core box it's 128, not 64); tune with `kotlinx.coroutines.io.parallelism` if you know you need more. +- `Dispatchers.Default` — CPU work. Sized = number of cores. +- `Dispatchers.Unconfined` — advanced use only; resumes on whatever thread completed the suspension point. Don't sprinkle it. +- **Inject dispatchers** for testing: `class Repo(private val io: CoroutineDispatcher = Dispatchers.IO)`. Makes `runTest` + `StandardTestDispatcher` actually usable. + +## Parallel work + +- Sequential (wrong for parallelism): + ```kotlin + val a = async { fetchA() }.await() + val b = async { fetchB() }.await() // only starts after a finishes! + ``` +- Parallel (correct): + ```kotlin + coroutineScope { + val a = async { fetchA() } + val b = async { fetchB() } + a.await() to b.await() + } + ``` +- `awaitAll(list)` on a collection of `Deferred` — propagates the first failure and cancels siblings. +- `supervisorScope { }` — used when one failing child **shouldn't** cancel siblings (fan-out to 10 endpoints where partial success is fine). + +## `StateFlow` vs `SharedFlow` + +- **`StateFlow`** — always has a current value; conflates (fast producers drop intermediate values). Use for "current state of something" (UI state, settings). Replay = 1 by contract. +- **`SharedFlow`** — for events (navigation, one-shot signals). Configure explicitly: `MutableSharedFlow(replay = 0, extraBufferCapacity = 64, onBufferOverflow = BufferOverflow.DROP_OLDEST)`. +- **Never a `SharedFlow` with `replay > 0` for events** — new subscribers will receive old events (toasts from 10 minutes ago). Use `replay = 0` + handle missed-signals differently. +- `StateFlow.value = x` is read-modify-write under contention — use `.update { it.copy(...) }` for correct atomic updates. + +## `Flow` correctness + +- `flow { }` is cold — the block runs per collector. Don't put side effects outside `emit` assuming they run once. +- `emit` is not thread-safe across multiple launched coroutines inside one `flow { }`. Use `channelFlow` / `callbackFlow` if you need multi-threaded emission. +- `flowOn(dispatcher)` affects **upstream only**. `flowOn(IO).map { }` — the `map` still runs on the downstream dispatcher. Put `flowOn` as late as possible, right before collection. +- `catch { }` catches only upstream exceptions. A throw inside `collect { }` goes to the caller, not to `catch`. This is exception transparency — don't violate by catching everything inside an operator. +- `Flow.first()` / `single()` / `toList()` on an infinite upstream hangs forever. Combine with `withTimeoutOrNull(...)`. + +## `SharingStarted` for hot Flows in UI + +- `MutableStateFlow(initial).asStateFlow()` — always active. +- `flow.stateIn(scope, SharingStarted.Eagerly, initial)` — active while scope alive. +- `flow.stateIn(scope, SharingStarted.Lazily, initial)` — activates on first subscriber, stays. +- `flow.stateIn(scope, SharingStarted.WhileSubscribed(5_000), initial)` — **the right default for mobile / screen-scoped state** (keeps alive 5 s after last subscriber to survive rotation without restart). + +## `callbackFlow` / `channelFlow` + +- `callbackFlow { }` to bridge listener-based APIs. **Must end with `awaitClose { unregister() }`** — otherwise the collector leaks the listener. +- Don't `trySend(x).getOrThrow()` — if the buffer is full, `trySend` returns a failure; decide between dropping, suspending (`send`), or increasing `BUFFERED` capacity. + +## Testing coroutines + +- `runTest { }` from `kotlinx-coroutines-test` — virtual time, skips delays automatically. +- Inject `TestDispatcher` into your scope: `StandardTestDispatcher()` (ordered, manual advance) or `UnconfinedTestDispatcher()` (immediate). +- `MainDispatcherRule` for Android / anything using `Dispatchers.Main`. +- `Turbine` library for asserting on `Flow` emissions deterministically. +- Don't call `Thread.sleep` in a coroutine test — it blocks virtual time. Use `delay(...)` and `advanceTimeBy(...)`. + +## Common anti-patterns + +| Anti-pattern | Correct | +|---|---| +| `GlobalScope.launch { ... }` | Inject `CoroutineScope` or use framework scope | +| `runBlocking { suspendCall() }` inside a suspend function | Just `suspendCall()` — remove `runBlocking` | +| `MutableStateFlow` returned from a public API | `val state: StateFlow = _state.asStateFlow()` | +| `.value = state.value.copy(x = y)` | `state.update { it.copy(x = y) }` | +| `flow { withContext(IO) { emit(...) } }` | `flow { emit(...) }.flowOn(IO)` | +| `try { work() } catch (e: Exception) { log(e) }` | Same plus `if (e is CancellationException) throw e` first | +| Parallel fan-out with `.map { async { it.fetch() } }.map { it.await() }` inside `List` | Wrap in `coroutineScope { ... awaitAll() }` for proper cancellation semantics | diff --git a/.agents/skills/kotlin-engineer/references/idioms.md b/.agents/skills/kotlin-engineer/references/idioms.md new file mode 100644 index 0000000000..877899aa2b --- /dev/null +++ b/.agents/skills/kotlin-engineer/references/idioms.md @@ -0,0 +1,116 @@ +# Kotlin idioms — API design policy & pitfalls + +Assumes you know scope functions, data/sealed/value classes, extension functions, inline/reified. This file is which to pick, and where LLMs pick wrong. + +## Scope functions — pick by intent + +| Function | Receiver is | Returns | Use for | +|---|---|---|---| +| `let` | `it` | lambda result | null-safe transform, introduce temporary scope | +| `run` | `this` | lambda result | compute a value from a receiver | +| `with(x) { }` | `this` | lambda result | same as `run` but non-extension (explicit target) | +| `apply` | `this` | receiver | configure a mutable object, return it | +| `also` | `it` | receiver | side-effect on a value (log, register), keep value | + +Rules: +- Only one of them per expression. Nesting `.apply { ... .let { ... } }` is a sign to extract a function. +- `?.let { }` for the "do X if non-null" pattern — not `if (x != null) ...`. +- Don't use `apply` to call a single function — just call it. `x.apply { foo() }` vs `x.also { it.foo() }` — prefer the one that matches return semantics (use `apply` when you're configuring many things on a builder). + +## Classes — pick the right kind + +- **`data class`** — pure value type with a primary constructor. Gives `equals` / `hashCode` / `copy` / `componentN`. Not for entities, services, or anything with identity / behavior. +- **`sealed interface` / `sealed class`** — closed hierarchies (result types, UI states, commands). Prefer `sealed interface` — lets implementations be `object` / `data object` / class, supports multiple interface inheritance. +- **`value class` (`@JvmInline`)** — domain primitives (`UserId`, `Email`, `Temperature`). Compiles to the underlying type, zero overhead. `init { require(...) }` for validation. +- **`object`** — stateless services, singletons, `Comparator` instances, companion objects for factory methods. +- **`data object`** — sealed-hierarchy singletons with nice `toString` (`Loading`, `Empty`). + +Pitfalls: +- `data class Foo(val id: Long, val name: String)` used as an entity → `equals` based on all fields changes with mutation, bugs in sets / maps. +- Value classes don't satisfy Java interop for `@Entity` / JPA — they're inlined and have no no-arg constructor. +- `copy()` calls the primary constructor, so `init` blocks **do** run — `require(...)` in `init` IS enforced on `copy()`. However, `copy()` bypasses secondary constructors and factory methods. If validation lives only there (not in `init`), `copy()` will skip it — put invariants in `init`. + +## Extension functions — when and where + +- **Extend types you don't own** to add domain-specific helpers (`String.isValidEmail()`). +- **Don't extend types you own** when a method would do — extensions are statically dispatched, so `open`-like polymorphism doesn't apply. +- **Never extend `Any` / `Any?`** — pollutes autocomplete for the whole project. +- **File-level** (top-level) extensions, not inside a class, unless the extension is conceptually a member of the enclosing class. +- Don't shadow members: an extension with the same name as a member is silently ignored. + +## `inline` / `reified` — when it earns its cost + +- `inline` removes lambda allocation — useful for **hot paths** with small lambdas (`measureTimeMillis { }`, locking helpers). +- `reified` — lets you use `T::class` inside the function, required for `Gson.fromJson()` style APIs. +- `inline` bloats call sites — don't inline large functions; compiler warns when body is large. +- `noinline` / `crossinline` are for specific scenarios (`noinline` to pass a lambda elsewhere, `crossinline` to disallow non-local returns from a lambda used in a different scope). Don't use them unless the compiler forces you. + +## Delegation + +- **Interface delegation** (`class A(b: B) : Iface by b`) — composition over inheritance, avoids boilerplate. +- **Property delegation** — `by lazy { ... }` (thread-safe by default), `by Delegates.observable(x) { _, old, new -> }`, `by map` / `by mutableMap` for config-like objects. +- `lateinit var` **doesn't support primitives or nullable types**. Use `Delegates.notNull()` for primitives; for nullable, just initialize to `null`. +- `lazy` has `LazyThreadSafetyMode.SYNCHRONIZED` by default; `.NONE` is faster for single-threaded code but unsafe across threads. + +## `Result` vs exceptions + +- Kotlin's `Result` is for **internal plumbing**, not public API (the `Result` type is generic-variance-limited and awkward from Java). +- For public APIs, throw domain exceptions (`UserNotFoundException`) or return a `sealed interface Outcome { data class Success(value: T); data class Failure(reason: Reason) }`. +- `runCatching { ... }` is handy but swallows `CancellationException` unless you re-check: + ```kotlin + runCatching { work() } + .onFailure { if (it is CancellationException) throw it } + ``` +- Don't use `runCatching` in coroutines as a general try/catch — rethrow cancellation explicitly. + +## Generics & variance + +- **`out T`** when `T` is a producer (read-only; `List` means "list of Animal or subtype"). **`in T`** when `T` is a consumer (write-only). +- **`where T : Comparable, T : Serializable`** for multiple bounds. +- `reified` requires `inline`. Can't be used with virtual methods. +- Star-projection `List<*>` when you don't care about the element type — read-only. +- Generic `T?` vs `T` matters: `fun first(list: List): T` forces non-null, even though the source may contain nulls — use `T?` for nullable semantics. + +## Immutability & collection choice + +- **Return `List` / `Map` / `Set`** from public APIs — read-only at Kotlin level (Java sees them as mutable; annotate or wrap if that matters). +- **`MutableList` / `MutableMap` only inside a function body** or as a private field when building. +- `emptyList()` / `listOf()` — immutable. `mutableListOf()` — `ArrayList`. Don't write `ArrayList()` directly when `mutableListOf` reads better. +- `buildList { add(...); addIf(...) }` — idiomatic for conditional construction without intermediate mutable var. + +## Null-safety traps + +- `String!` (platform type) in API responses is a footgun — explicitly handle: `response.body ?: error("empty body")`. +- `list.filter { ... }.first()` vs `list.first { ... }` — the first chains through all elements; `first { }` short-circuits. +- `Map[key]` returns `V?`. `Map.getValue(key)` returns `V` but throws on missing — pick based on contract. +- `lateinit var x: SomeType` — accessing before init throws `UninitializedPropertyAccessException`, not NPE. `::x.isInitialized` to check. + +## Equality & hashing + +- `==` is structural (`equals`), `===` is referential. In Java interop code review, watch for Kotlin `==` being translated to Java `.equals` — fine, but `null == null` works both ways. +- Data classes generate `equals` / `hashCode` based on primary-constructor fields. Fields declared in the body are not included — don't rely on it. +- For value classes, `equals` compares underlying values directly. + +## Enums vs sealed + +- **Enum** — known, fixed set of instances without per-case data (`enum class Color { RED, GREEN, BLUE }`). +- **Sealed interface with `data object`s** — when cases may carry data or you want exhaustive `when` with mixed shapes. +- Don't emulate sealed hierarchies with enums + `when (color) { BLUE -> "blue"; else -> error("?") }` — add a case and the `else` swallows it. + +## Domain modelling — non-obvious policy + +- **Do not return `kotlin.Result` from public API.** It's designed for `runCatching` plumbing, doesn't compose with Java callers, and its variance-limited generics break generic wrappers. Convert at the boundary (throw, or map to a sealed domain type). +- **`Either` from Arrow** — only if the project already depends on Arrow. Don't pull it in for a single function. +- `T?` must mean one thing. If "absent" and "unknown" are both possible, use a sealed hierarchy — overloading `null` is a recurring source of silent bugs. + +## When you'd reach for Java-idiom and Kotlin has a better one + +| Java-style | Kotlin-style | +|---|---| +| `if (x == null) throw IllegalArgumentException("x")` | `requireNotNull(x) { "x" }` | +| `Objects.requireNonNull(x)` | `requireNotNull(x)` | +| `new ArrayList<>(list)` to copy | `list.toMutableList()` | +| `map.getOrDefault(k, default)` | `map[k] ?: default` | +| Manual `Builder` class | primary constructor + `copy()` + default args | +| Anonymous inner class for a single method | lambda or function reference | +| `if (x != null) x.foo() else null` | `x?.foo()` | diff --git a/.agents/skills/kotlin-review/SKILL.md b/.agents/skills/kotlin-review/SKILL.md index 6cbca1afe8..f1f2a01213 100644 --- a/.agents/skills/kotlin-review/SKILL.md +++ b/.agents/skills/kotlin-review/SKILL.md @@ -25,9 +25,14 @@ live in `.agents/`: 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. + Apply the `AGENTS.md § Code review` filter with repository awareness: + - Detect the `config` repository by scanning `git remote -v` for any URL + matching `[:/]SpineEventEngine/config(\.git)?$`. + - In **`config` itself**, skip only `gradlew` and `gradlew.bat`; every other + config-distributed path is owned by this repo and stays in scope. + - In any **consumer repo**, skip the full config-distributed list. 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`: diff --git a/.agents/skills/kotlin-review/agents/openai.yaml b/.agents/skills/kotlin-review/agents/openai.yaml new file mode 100644 index 0000000000..7497fb9b57 --- /dev/null +++ b/.agents/skills/kotlin-review/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Kotlin Review" + short_description: "Review Kotlin and Java code changes" + default_prompt: "Use $kotlin-review to review Kotlin and Java changes against Spine coding guidelines, safety rules, and testing policy." diff --git a/.agents/skills/move-files/SKILL.md b/.agents/skills/move-files/SKILL.md index b92b05d3f5..ccff78bc92 100644 --- a/.agents/skills/move-files/SKILL.md +++ b/.agents/skills/move-files/SKILL.md @@ -42,7 +42,7 @@ description: > - 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 + Run the `version-bumped` skill 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 diff --git a/.agents/skills/pre-pr/SKILL.md b/.agents/skills/pre-pr/SKILL.md index 591b26b308..7c51b4da4b 100644 --- a/.agents/skills/pre-pr/SKILL.md +++ b/.agents/skills/pre-pr/SKILL.md @@ -41,24 +41,35 @@ the first failure. - 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` +- Repository kind: detect the `config` repository by scanning `git remote -v` + for any URL matching `[:/]SpineEventEngine/config(\.git)?$`. +- Filter changed files using `AGENTS.md § Code review`: + - In **`config` itself**, skip only `gradlew` and `gradlew.bat`; every other + config-distributed path is owned by this repo and stays in scope. + - In any **consumer repo**, remove the full config-distributed skip list. 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`. - 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. +- Hugo site: detect a site only when `docs/` or `site/` contains one of + `hugo.toml`, `hugo.yaml`, `config/hugo.toml`, `config/hugo.yaml`, + `config/_default/hugo.toml`, or `config/_default/hugo.yaml`. - 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) + - **site** — a Hugo site exists and any file under `docs/**` or `lychee.toml` + changed (triggers Hugo link check; pure `README.md` or KDoc-only changes do + *not* count). If `lychee.toml` changes but no Hugo site exists, keep + `site=false` and note that `check-links` is not applicable if the skipped + reviewer needs explanation. ### 2. Version-bump check @@ -68,8 +79,8 @@ the first failure. 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. + `.agents/skills/bump-version/` exists, **auto-fix immediately** by running + the `bump-version` skill 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 @@ -93,10 +104,14 @@ 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) +### 4. Reviewers -Dispatch relevant reviewers concurrently; collect all verdicts before -aggregating. Before dispatching, check that the skill directory exists under +Run every relevant reviewer and collect all verdicts before aggregating. In a +single Codex session, run the reviewer skills one by one and batch independent +search/read commands inside each reviewer. If multi-agent tools are available, +parallel reviewer dispatch is optional but not required. + +Before running a reviewer, 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. @@ -108,9 +123,9 @@ for this repo" rather than failing. **`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`" +the link check 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. +HEAD). Otherwise run `check-links` 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 @@ -184,5 +199,5 @@ them in one line before the verdict: `Auto-fixed: .` - 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`; +- This skill may auto-fix a missing version bump by running the `bump-version` skill; all other fixes require explicit user confirmation. diff --git a/.agents/skills/pre-pr/agents/openai.yaml b/.agents/skills/pre-pr/agents/openai.yaml new file mode 100644 index 0000000000..6964e9d975 --- /dev/null +++ b/.agents/skills/pre-pr/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Pre-PR" + short_description: "Run the repo pre-PR verification gate" + default_prompt: "Use $pre-pr to run the repository pre-PR checklist, including the version gate, build or doc check, and relevant reviewers." diff --git a/.agents/skills/raise-coverage/SKILL.md b/.agents/skills/raise-coverage/SKILL.md new file mode 100644 index 0000000000..790007056e --- /dev/null +++ b/.agents/skills/raise-coverage/SKILL.md @@ -0,0 +1,241 @@ +--- +name: raise-coverage +description: > + Raise JVM test coverage for a Gradle module or source path. 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**. Then localizes + uncovered lines and branches from Kover's JaCoCo-format XML report, and + generates policy-compliant unit tests — stubs not mocks; tests are written + in **Kotlin** with Kotest assertions, regardless of whether + the code under test is Kotlin or Java; class names use the **`Spec`** + suffix. Proposes a test-case list and waits for approval before writing any + test, then re-runs the report to confirm the gap is closed. Use when asked + to add missing tests, close coverage gaps, or raise a module's coverage. +--- + +# Raise test coverage + +You localize untested code with **Kover**'s JaCoCo-format XML report and write +the unit tests that close the gap. Work on one Gradle module or path at a time, +always propose the test-case list and **wait for approval** before writing, +and verify the gap is actually closed afterward. + +Before the main flow runs, you ensure the repo is on Kover. If vanilla JaCoCo +is detected anywhere, you propose a one-shot **repo-wide migration to Kover** +and wait for approval. The mechanical recipe lives in +[`references/migrate-to-kover.md`](references/migrate-to-kover.md). + +The authoritative standards live in `.agents/`: + +- `.agents/testing.md` — stubs not mocks; Kotest assertions; cover API edge + cases; scaffold `when`/sealed-class branches. +- `.agents/coding-guidelines.md` — Kotlin/Java idioms for the tests you write. +- `.agents/version-policy.md` — tests-only changes do not require a version bump. + +Mechanical detail (report paths, XML parsing, gap rules) lives in +[`references/coverage-signals.md`](references/coverage-signals.md). Keep this +file about *what to do*; that one is *how to read the numbers*. + +## Scope + +- **Coverage comes from Kover's local report.** Spine consumer repos apply the + Kover Gradle plugin with `useJacoco(version = Jacoco.version)`, which makes + Kover compute coverage with the JaCoCo engine and emit JaCoCo-format XML. + Per-module task `::koverXmlReport`; XML at + `/build/reports/kover/report.xml`. KMP modules configured by Spine's + `kmp-module` script plugin define only the `total` Kover report, so the + same `koverXmlReport` / `report.xml` pair applies — see + `references/coverage-signals.md`. +- **Target human-written `src/main` code only.** Never write tests for generated + code (any path containing `generated`, e.g. Protobuf output), `examples`, or + existing test sources. These are excluded by `.codecov.yml` — respect that + boundary. +- **One module or path per run.** + +## Inputs + +`$ARGUMENTS` is one of: + +- a Gradle module path — e.g. `:base`, `:core`; +- a source file or directory — e.g. `base/src/main/kotlin/io/spine/...`; +- `--triage` — read-only: produce a ranked gap report for the repo (or the named + module) and stop, without proposing or writing tests. + +If `$ARGUMENTS` is empty, ask which module or path to target (or offer +`--triage` to help choose). + +## Step 0 — Ensure Kover + +Run this **before** the Workflow below. Behaviour depends on `$ARGUMENTS`: + +### Under `--triage` (read-only) + +`--triage` is contractually read-only and must not write build files. If +Kover is not already applied everywhere, **emit a "Setup required" report +and stop** without writing anything (and without proposing a migration). +List the modules that still need migration, point at +[`references/migrate-to-kover.md`](references/migrate-to-kover.md), and tell +the user to re-run `/raise-coverage` **without** `--triage` to perform the +migration first. Once Kover is in place everywhere, `--triage` proceeds to +the Workflow. + +### Otherwise + +Branch on the repo's current coverage setup (detection patterns and full +migration recipe in +[`references/migrate-to-kover.md`](references/migrate-to-kover.md)): + +1. **Kover applied everywhere already** — silently proceed to the Workflow. +2. **No coverage plugin anywhere** — silently install Kover (per the recipe). + Record "Migration: installed Kover" in the final Report. No approval gate + for this branch. +3. **Vanilla JaCoCo in ≥1 module** (with or without Kover alongside) — emit a + proposal and **wait for approval** before making any edits. + +### Proposal output + +Emit the following Markdown sections, in this order, then stop and wait for approval: + +- **Detected** — every module applying `jacoco` / `JacocoPlugin` / + `JacocoConfig.applyTo` / a `jacoco-*.gradle.kts`; annotate "vanilla only" + vs. "JaCoCo+Kover both"; note any root `jacocoRootReport`. Treat a root-level + `KoverConfig.applyTo(rootProject)` as a Kover signal (it is the Kover-based + successor to `JacocoConfig.applyTo`). +- **Plan** — every file that will be edited, with paths: per-module + `build.gradle.kts`, root `build.gradle.kts`, `.codecov.yml`, + `.github/workflows/*.yml`, `scripts/*.sh`. +- **Translation notes** — the rows from the translation table in + `references/migrate-to-kover.md` that apply to this repo. +- **Manual-review surfaces** — items from that file's "Manual-review + surfaces" list that the user must decide on before the migration can + proceed. +- **Smoke check that will follow** — the commands listed in + *Verify (smoke check)* below. +- Close with: "Confirm to apply, or call out anything to change first." + +### Wait, then apply + +Do not write any file until the user explicitly says "go" / "yes" / "apply" +(or equivalent). On adjustment requests, regenerate the proposal and wait +again. After approval, apply the migration per +`references/migrate-to-kover.md`, logging `edited ` per file. Any +unresolved manual-review surface → stop with "needs your call on ``". + +### Verify (smoke check) + +Pick the smallest migrated leaf module and run `::koverXmlReport`, +then inspect `/build/reports/kover/report.xml`. KMP modules also use +this task — Spine's `kmp-module` script plugin configures only Kover's +`total` report, which for the JVM-only KMP target is identical in shape to +the JVM case (see `references/migrate-to-kover.md` §6). + +Run `./gradlew ::koverXmlReport --quiet`; if the root was touched, +also run `./gradlew koverXmlReport --quiet`. +Confirm the XML exists, is non-empty, and the first non-XML-declaration line +contains `:koverXmlReport` (the same task on JVM and KMP modules + configured by Spine's convention plugins; see + `references/coverage-signals.md`). The report task runs the tests first. + - Parse the XML for uncovered lines (`ci == 0`) and partially covered + branches (`mb > 0`). Prioritize methods whose `BRANCH` counter has + `missed > 0`. + - Drop any class under an excluded path (generated / examples / test). + - Discard **non-actionable** gaps the engine cannot credit even with a + perfect test (see `references/coverage-signals.md`): Kotlin `inline` / + `inline reified` functions (their bytecode is inlined into each call + site, so the definition lines stay `ci=0` regardless of tests), + unreachable guards (`require`/`check`/`error` paths the public API + cannot trigger), and `throw helper(...)` lines where the helper throws + internally. Report these as non-actionable instead of proposing tests for + them. + +3. **Read before you write.** + - Read the class(es) under test in full — public API, constructors, branch + conditions, `when`/sealed exhaustiveness, error paths. + - Read existing tests in the module to match structure, naming, fixtures, + and the test source set/layout you will add to. + - Read collaborators you will need to substitute, so you can write **stubs** + (hand-written fakes), not mocks. + +4. **Propose the test cases — then WAIT.** + - For each target, list the concrete cases: the method/branch, the input, + the expected outcome, and the stub(s) required. Map each case back to the + uncovered line/branch it closes. + - Present this list and **wait for the user's confirmation** before writing + anything. (Under `--triage` you already stopped at step 1.) + +5. **Generate the tests** (only after approval), per `.agents/testing.md`: + - **Write tests in Kotlin**, regardless of whether the code under test is + Kotlin or Java. Use JUnit Jupiter structure (`@Test` / `@Nested` / + `@DisplayName`) with **Kotest assertions** (`shouldBe`, `shouldThrow`, + `shouldContainExactlyInAnyOrder`, …). Reach for the + `truth-proto-extension` only when asserting on Protobuf message subjects + that Kotest's matchers cannot express, and keep that import isolated to + the case that needs it. + - **Class names use the `Spec` suffix** — e.g. `AbstractSourceFileSpec`, + not `AbstractSourceFileTest`. This matches the house convention in + existing `*Spec.kt` files (`base-libraries`, etc.) and applies even when + the code under test is Java. + - **Stubs, not mocks.** No mocking framework is on the classpath by design. + - Cover API edge cases; add a case per `when`/sealed-class branch. + - Place the test under `/src/test/kotlin/...`, mirroring the + package of the code under test (KMP: `src/jvmTest/kotlin/...` or + `src/commonTest/kotlin/...` per the module's target). Reuse the file's + copyright header. + +6. **Verify.** + - Re-run `::koverXmlReport`. + - Confirm the previously-listed uncovered `nr` lines/branches no longer + appear as gaps, and the class's `LINE` / `BRANCH` `missed` counters + dropped. + - Confirm the module total does not regress against `.codecov.yml`. + - If a test fails to compile or the gap is not closed, fix and re-run before + reporting done. + +## Report + +Return five sections (the **Migration** section is emitted only when Step 0 +actually did work): + +- **Migration** — what Step 0 changed, with the list of edited files and the + smoke-check result. Omit when Step 0 was a no-op (Kover already in place). +- **Gaps** — uncovered lines/branches found (file → lines/branches). +- **Proposed cases** — the awaited list from step 4. +- **Generated** — test files added, with the cases each covers. +- **Verification** — before/after coverage for the target, and confirmation that + no `.codecov.yml` target regressed. + +## Safety + +- **`--triage` is read-only.** Step 0 never writes under `--triage`; if + Kover is not in place, emit "Setup required" and stop. +- **Migration requires approval when vanilla JaCoCo is detected.** Silent + install of Kover happens only when *no* coverage frontend is in place and + `--triage` is not requested. +- **Read-only until approval.** Do not write tests before the user confirms the + step-4 list. +- **Never weaken a `.codecov.yml` target** or extend its `ignore` list to make a + check pass. +- **Never add a mocking dependency** (Mockito, MockK, …) — write stubs. +- **No version bump.** Tests-only changes do not require one; do not invoke + `/version-bumped` for a tests-only result. If you had to touch production code + to make it testable, that is a separate change that needs its own review and a + version bump. The migration itself (Step 0) **does** alter build files and is + not tests-only — treat it as production-code change for version-bump purposes + when it runs. diff --git a/.agents/skills/raise-coverage/agents/openai.yaml b/.agents/skills/raise-coverage/agents/openai.yaml new file mode 100644 index 0000000000..32a4ed1f9f --- /dev/null +++ b/.agents/skills/raise-coverage/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Raise Coverage" + short_description: "Migrate to Kover if needed, then generate unit tests to close coverage gaps." + default_prompt: "Use $raise-coverage. Step 0 first: detect the coverage setup. If vanilla JaCoCo is found anywhere, propose a one-shot repo-wide migration to Kover and wait for approval before editing. If no coverage frontend is in place, install Kover silently. Smoke check after migration: run `./gradlew ::koverXmlReport --quiet` and confirm `/build/reports/kover/report.xml` is non-empty. KMP modules use the same task — Spine's `kmp-module` script plugin configures only Kover's `total` report, and the JVM-only KMP target produces a JVM-shaped XML there. Then run the normal flow: localize uncovered lines and branches from Kover's JaCoCo-format XML report, propose a test-case list and wait for approval, then generate policy-compliant unit tests (stubs not mocks; written in Kotlin with Kotest assertions regardless of whether the code under test is Kotlin or Java; class names use the `Spec` suffix, e.g. `AbstractSourceFileSpec`) and re-run the same report task to verify the gaps are closed. Tests-only changes do not require a version bump." diff --git a/.agents/skills/raise-coverage/references/coverage-signals.md b/.agents/skills/raise-coverage/references/coverage-signals.md new file mode 100644 index 0000000000..1944ffd55d --- /dev/null +++ b/.agents/skills/raise-coverage/references/coverage-signals.md @@ -0,0 +1,181 @@ +# Coverage signals — localization & verification + +Mechanical reference for the `raise-coverage` skill. The `SKILL.md` says *what to +do*; this file says *how to read the numbers*. + +Coverage is computed by the **JaCoCo engine**, but the Spine convention is to +expose it through **Kover** with `useJacoco(version = Jacoco.version)`. Kover +owns the Gradle tasks; JaCoCo owns the engine and the XML format. The skill's +Step 0 ensures every target repo is on Kover before any analysis runs (see +[`migrate-to-kover.md`](migrate-to-kover.md)). + +## Where the report lives + +Kover is applied per module via the distributed `jvm-module` / +`kmp-module` script plugins, or directly: + +```kotlin +plugins { /* … */ id("org.jetbrains.kotlinx.kover") } +kover { + useJacoco(version = Jacoco.version) // compute coverage with the JaCoCo engine + reports.total.xml.onCheck = true // emit XML on `check` +} +``` + +`useJacoco(...)` is a **Kover** DSL call — the tasks are Kover's, but the +engine and the XML format are JaCoCo's. + +- Per-module report task: `::koverXmlReport` +- XML path: `/build/reports/kover/report.xml` +- Same task on KMP modules configured by Spine's `kmp-module` script + plugin — it only sets up the `total` report, so `koverXmlReport` exists + but no `koverXmlReport` does (a `Jvm`-suffixed task would only + appear if a named `variant("jvm") { … }` block were declared). +- Root-level aggregation (when the repo wires it): + `./gradlew koverXmlReport` → `build/reports/kover/report.xml` + +If unsure of the output path: + +```bash +find /build -name '*.xml' -path '*kover*' +``` + +## Generating a report + +```bash +# Kover — runs the module tests, then writes report.xml. Same task name +# for Kotlin-JVM and Spine `kmp-module` modules. +./gradlew ::koverXmlReport +``` + +## Reading the XML + +Kover emits the JaCoCo XML structure: `report > package > class > method`, each +with `` elements, plus `` elements carrying per-line data. + +- `` — totals at each level. +- `` — per source line (inside ``). + +### Gap rules + +- **Uncovered line**: `ci == 0` (and `mi > 0`). +- **Partially covered branch**: `mb > 0` (regardless of `cb`). +- **High-value targets**: methods whose `BRANCH` (or `LINE`) counter has + `missed > 0` — enumerate these first in the `SKILL.md` step-4 list. + +### Non-actionable gaps (recognize and skip) + +Some lines show as uncovered but cannot gain coverage from *any* test — do not +propose tests for them; report them as non-actionable: + +- **Kotlin `inline` / `inline reified` functions.** The compiler inlines the body + into every call site, so the engine credits the caller, not the definition. The + definition lines stay `ci=0` even when fully exercised. (Verified on + `base-libraries`: `parse(...)` reified overloads remained `ci=0` after a + passing round-trip test.) +- **Unreachable guards.** `require` / `check` / `error` branches the public API + cannot trigger (e.g. an invariant guaranteed by construction) — the gap is real + but unclosable from outside. +- **`throw helper(...)` where `helper` always throws.** Spine's `Exceptions` + utilities (`newIllegalStateException`, `newIllegalArgumentException`, …) are + *declared* to return an exception but actually throw it internally. Callers + still write `throw newIllegalStateException(...)` to satisfy the compiler's + flow analysis, but control never returns to the caller's `ATHROW`. JaCoCo + attributes coverage at the line's downstream probe, which is never hit, so + the whole line shows `mi=N ci=0` even when a test exercises the catch block + and asserts on the exception's message. (Verified on `base-libraries`: + `AbstractSourceFile.java:69` and `:82` remained `mi=10 ci=0` after passing + tests that drove the `IOException` paths in `load()` and `store()` and + asserted on the wrapped message.) + +### Extracting gaps for a class + +The XML carries a `DOCTYPE` pointing at a public DTD, so always pass `--nonet` to +`xmllint` (or use the Python recipe) — parsing must never reach the network. The +report has no XML namespace, so the XPath is plain. + +Note that JaCoCo (and therefore Kover with `useJacoco(...)`) puts the +`` elements under ``, not under `` — the `` +element only carries summary ``s. To get the uncovered-line gaps, +query by the `` that holds the class's source, scoped to the +class's package: + +```bash +# Package of the FQN with '/' as the separator; source file is the simple +# class name plus the language suffix (.java or .kt). +xmllint --nonet \ + --xpath '//package[@name="io/spine/foo"]/sourcefile[@name="MyType.java"]/line[@ci="0" or @mb > 0]' \ + /build/reports/kover/report.xml +``` + +To confirm a method-level branch gap inside that class, query the `` +element's `` counters: + +```bash +xmllint --nonet \ + --xpath '//class[@name="io/spine/foo/MyType"]/method[counter[@type="BRANCH" and @missed>0]]/@name' \ + /build/reports/kover/report.xml +``` + +Python (robust for large reports; reads both class/method counters and +sourcefile lines): + +```python +import xml.etree.ElementTree as ET +root = ET.parse("report.xml").getroot() +for pkg in root.findall("package"): + for sf in pkg.findall("sourcefile"): + gaps = [l.get("nr") for l in sf.findall("line") + if l.get("ci") == "0" or int(l.get("mb", "0")) > 0] + if gaps: + print(pkg.get("name"), sf.get("name"), gaps) +``` + +## What is in scope + +Only human-written `src/main` code. Two filters already exclude the rest — honor +both, and never count an excluded file as a gap: + +- **Kover filters** — `kover { filters { excludes { … } } }` drops classes by + pattern. Generated paths (anything containing `generated`, including Protobuf + and `protoc-gen-kotlin` output) are excluded by convention. +- **`.codecov.yml`** — `ignore` removes `**/generated/**`, `**/examples/**`, + `**/test/**`; coverage status applies only to `src/main/**`. + +## KMP / Kotlin-JVM modules + +For both Kotlin-JVM and KMP modules configured by Spine's `kmp-module` script +plugin, `koverXmlReport` is the single report task — Kover only generates +`koverXmlReport` tasks when a named `variant("…") { … }` block is +declared, and `kmp-module` declares none (it configures only the `total` +report). Add tests under the module's test source set (`src/test`, or +`src/jvmTest` / `src/commonTest` for KMP) to match. + +## Verification (SKILL.md step 6) + +After generating tests, re-run `::koverXmlReport` and re-parse the XML +for the targeted class: the previously listed `nr` values should no longer be +gaps, and the method/class `BRANCH` + `LINE` counters should show `missed` +reduced. Cross-check the module total against the relevant `.codecov.yml` +`project` target so nothing regresses. + +--- + +## Appendix — future: Codecov triage tier (deferred) + +v1 is local-report-only. A later iteration may add a Codecov triage tier to pick +targets across repos without a local build. If added: + +- Base `https://api.codecov.io/api/v2`; `service = github`, + `owner = SpineEventEngine`, `repo = `; auth header + `Authorization: Bearer $CODECOV_API_TOKEN`. +- Useful endpoints: per-file `totals`, line-by-line `report`, single + `file_report`, and `commits` (for trend). Filters: `path`, `flag`, + `component_id`. +- Read the per-line hit/miss/partial encoding from live JSON once — do not + hardcode it; it is easy to get backwards. +- Always degrade gracefully to the local report (above) when the token is absent. + +Until that lands, do everything from the local Kover report. diff --git a/.agents/skills/raise-coverage/references/migrate-to-kover.md b/.agents/skills/raise-coverage/references/migrate-to-kover.md new file mode 100644 index 0000000000..7550013d33 --- /dev/null +++ b/.agents/skills/raise-coverage/references/migrate-to-kover.md @@ -0,0 +1,352 @@ +# Migrate from vanilla JaCoCo to Kover + +Mechanical recipe for the `raise-coverage` skill's Step 0. The skill detects +vanilla JaCoCo in a consumer repo, proposes the migration, waits for approval, +and then applies the edits below. The convention is **Kover Gradle plugin** with +the JaCoCo engine via `useJacoco(version = Jacoco.version)` — JaCoCo-format XML +is preserved, only the Gradle plugin and task names change. + +## 1. Purpose + +Stand the target repo up on Kover so the rest of the `raise-coverage` skill can +run against a single coverage frontend. After migration, every coverage path +goes through Kover — per-module `koverXmlReport`, root-level `koverXmlReport` +for aggregation, and JaCoCo-format XML at `build/reports/kover/report.xml`. + +References: + +- Kover Gradle plugin docs: +- Kover migration guide (0.6.x → 0.7+): + + +## 2. Detection signals + +Walk every Gradle module's `build.gradle.kts`. Parse `settings.gradle.kts` for +`include(...)`; honor `project(":x").projectDir = file(...)` overrides. + +For each module, grep with the patterns below. + +### Vanilla JaCoCo applied + +- Plugin block in `plugins { … }`: + - `^\s*jacoco\b` + - `id\("jacoco"\)` +- Imperative apply: + - `apply\(\)` + - `apply\(plugin = "jacoco"\)` +- Spine script plugins distributing JaCoCo: + - `apply\(plugin = "jacoco-` (covers `jacoco-kotlin-jvm`, `jacoco-kmm-jvm`) +- Spine multi-module aggregation helper: + - `JacocoConfig\.applyTo` + - import `io.spine.gradle.report.coverage.JacocoConfig` +- DSL blocks (configuration without explicit plugin id): + - `jacoco\s*\{` + - `jacocoTestReport\s*\{` + - `jacocoTestCoverageVerification\s*\{` + - `tasks\.named\("jacoco` +- Root-level aggregation: + - `jacocoRootReport` + +### Kover already applied (anywhere on this module) + +- Plugin id directly: + - `org.jetbrains.kotlinx.kover` +- Spine script plugins that auto-apply Kover: + - `id\("jvm-module"\)` — applies Kover at `jvm-module.gradle.kts:54` + and configures it at `jvm-module.gradle.kts:99`. + - `id\("kmp-module"\)` — applies Kover at `kmp-module.gradle.kts:74` + and configures it at `kmp-module.gradle.kts:181`. +- Spine multi-module Kover aggregation helper (root project only): + - `KoverConfig\.applyTo` + - import `io.spine.gradle.report.coverage.KoverConfig` + +### Outcome + +Classify each module as one of: + +| State | Action | +|---|---| +| Kover only | nothing to do | +| Kover + vanilla JaCoCo | strip JaCoCo, keep Kover (decision 4) | +| Vanilla JaCoCo only | migrate to Kover | +| Neither | silent install of Kover (no approval gate) | + +If at least one module is "vanilla JaCoCo only" or "Kover + vanilla JaCoCo", +the skill emits the migration proposal and waits. + +## 3. Per-module migration + +Apply these edits to each module's `build.gradle.kts`: + +### Add Kover + +Gradle's `plugins { }` block is a constrained DSL that accepts **literal** +plugin IDs and versions only — non-literal constants from `buildSrc` are not +guaranteed to resolve there across the Gradle versions Spine targets. Use +literals; the `Kover` / `Jacoco` constants in `io.spine.dependency.test` +still source-of-truth the values you paste in. + +- If the module already applies `jvm-module` or `kmp-module`, **skip this + step** (log "already via jvm-module" / "already via kmp-module") — both + script plugins auto-apply Kover. +- If `buildSrc` is on the classpath (the normal Spine consumer case), use the + bare literal — `buildSrc/build.gradle.kts` pins the Kover plugin version + globally via the `koverVersion` property, so a per-module version pin is + redundant: + ```kotlin + plugins { + id("org.jetbrains.kotlinx.kover") // matches `io.spine.dependency.test.Kover.id` + } + ``` +- Without `buildSrc`, pin the version literally (substitute the current + `io.spine.dependency.test.Kover.version` value): + ```kotlin + plugins { + id("org.jetbrains.kotlinx.kover") version "0.9.8" + } + ``` + +### Strip JaCoCo + +- Remove `jacoco` from `plugins { }` (or the `id("jacoco")` line, or + `apply()`, or `apply(plugin = "jacoco")`). +- Replace `apply(plugin = "jacoco-kotlin-jvm")` / `apply(plugin = "jacoco-kmm-jvm")` + with `id("jvm-module")` / `id("kmp-module")` when that is the module's role; + otherwise drop and add `id("org.jetbrains.kotlinx.kover")` directly (the + literal value of `io.spine.dependency.test.Kover.id`; the Gradle Kotlin DSL + `plugins { }` block does not accept buildSrc constants across the Gradle + versions Spine supports). +- Rewrite `JacocoConfig.applyTo(rootProject)` (at the root build script) to + `KoverConfig.applyTo(rootProject)` and update the import to + `io.spine.gradle.report.coverage.KoverConfig`. The Kover-based helper is the + documented successor — it wires the Kover plugin at the root, adds + `kover(project(...))` for every subproject that applies Kover, configures + `useJacoco(version = Jacoco.version)`, and pushes the generated-class FQNs + into both the per-module and the root `kover { reports { filters { … } } }` + blocks. See §4 (root aggregation) for the long-form equivalent if `buildSrc` + is not on the classpath. +- **Lifecycle gotcha — do not call `KoverConfig.applyTo(...)` from inside + `gradle.projectsEvaluated { … }`.** Many Spine consumer repos wrap + `JacocoConfig.applyTo(project)` in that block; carrying the pattern over + fails with `Cannot run Project.afterEvaluate(Action) when the project is + already evaluated`, because Kover's plugin registers its own `afterEvaluate` + hooks at apply time. Lift the call to top level in the root build script. + `KoverConfig` configures the root eagerly and uses + `pluginManager.withPlugin(...)` callbacks for subprojects, so modules that + apply Kover later in the same configuration phase are still discovered + before Kover finalizes its reports. + +### Translation table + +| JaCoCo construct | Kover / action | +|---|---| +| `jacoco { toolVersion = Jacoco.version }` | drop (engine version moves to root `useJacoco(...)`) | +| `jacoco { toolVersion = "" }` | **flag** (intentional engine pin — confirm Kover's `useJacoco(version = ...)` matches) | +| `reports { xml=true; html=true; csv=false }` on `jacocoTestReport` | `kover { reports { total { xml { onCheck = true }; html { } } } }` | +| `executionData.setFrom(...)` | **flag** (Kover manages exec data internally) | +| `sourceDirectories.setFrom(...)` | **flag** (Kover infers from compilations) | +| `classDirectories.setFrom(...)` — the Kotlin-JVM/KMP `walkBottomUp` recipe used by `jacoco-kotlin-jvm` / `jacoco-kmm-jvm` | drop; **flag** if the module is non-Kotlin (Kover may not pick up its classes) | +| `reports.xml.outputLocation.set(...)` | **flag** (Kover fixes the path; consumers must follow) | +| `tasks.named("jacocoTestReport") { dependsOn(...) }` | rewrite to `tasks.named("koverXmlReport") { dependsOn(...) }` | +| `violationRules { rule { limit { counter; value; minimum } } }` on `jacocoTestCoverageVerification` | `kover { reports { verify { rule { … } } } }` — counter map below | + +### Counter mapping + +JaCoCo `counter` → Kover `bound { counter = … }`: + +| JaCoCo | Kover | +|---|---| +| `INSTRUCTION` | `INSTRUCTION` | +| `BRANCH` | `BRANCH` | +| `LINE` | `LINE` | +| `METHOD` | `INSTRUCTION` (no direct equivalent) — **flag** | +| `CLASS` | no equivalent — **flag** | + +`value` maps directly (`COVEREDRATIO`, `MISSEDRATIO`, `COVEREDCOUNT`, +`MISSEDCOUNT`). `minimum` / `maximum` map directly. + +### Simplification with `jvm-module` / `kmp-module` + +If the module's role is the standard Spine JVM (or KMP) module, replace the +JaCoCo bits with `id("jvm-module")` (or `id("kmp-module")`). Both script plugins +already apply Kover and configure `useJacoco(...)` plus the XML report — the +migration becomes "remove JaCoCo and let the convention plugin take over". + +## 4. Root-level aggregation + +Apply at the root only if the source repo had `jacocoRootReport` **or** has more +than one module to aggregate. Skip if the root already applies `jvm-module` +(unusual but possible). + +### Preferred — `KoverConfig.applyTo(rootProject)` + +When `buildSrc` is on the classpath (the standard Spine setup), use the helper +in `io.spine.gradle.report.coverage.KoverConfig`. It applies Kover at the root, +adds a `kover(project(...))` dependency for every subproject that applies +Kover, configures `useJacoco(version = Jacoco.version)`, and excludes classes +compiled from `generated/` source directories from both per-module and root +reports. + +```kotlin +// Root build.gradle.kts +import io.spine.gradle.report.coverage.KoverConfig + +KoverConfig.applyTo(rootProject) +``` + +This is the documented successor to `JacocoConfig.applyTo(rootProject)` and is +what the skill writes when migrating consumer repos. + +### Long-form — when `buildSrc` is not available + +The `Kover` and `Jacoco` constants live in `buildSrc/.../io/spine/dependency/test/` +and are unreachable when this fallback applies. Paste the literal values +(substitute the current `Kover.version` / `Jacoco.version`): + +```kotlin +// Root build.gradle.kts +plugins { + id("org.jetbrains.kotlinx.kover") version "0.9.8" +} + +dependencies { + kover(project(":foo")) + kover(project(":bar")) + // … one entry per consuming module +} + +kover { + useJacoco(version = "0.8.14") + reports { + total { + xml { onCheck = true } + html { } + } + } +} +``` + +Note: the long-form variant does **not** exclude generated code automatically. +Either also apply `KoverConfig.applyTo(rootProject)` (preferred, but requires +`buildSrc`), or push your own exclusion patterns into +`kover { reports { filters { excludes { classes(…) } } } }` at the root and in +each subproject. + +If the source repo had a root-level `jacocoTestCoverageVerification` +(`violationRules`), mirror its `rule { limit { … } }` blocks to +`kover { reports { verify { rule { bound { … } } } } }` at the root using the +counter mapping above. Do **not** add root-level rules when the source repo had +none. + +## 5. CI, `.codecov.yml`, scripts — substitutions + +Apply globally (preserve case in surrounding tokens): + +| Old | New | +|---|---| +| `jacocoTestReport` | `koverXmlReport` | +| `jacocoRootReport` | `koverXmlReport` (root) | +| `build/reports/jacoco/test/jacocoTestReport.xml` | `build/reports/kover/report.xml` | +| `build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml` | `build/reports/kover/report.xml` | + +### `.github/workflows/*.yml` + +Substitute task and path tokens as above. If a step uploads the JaCoCo XML to +Codecov, update the `files:` glob to `**/build/reports/kover/report.xml`. + +### `.codecov.yml` + +Substitute path tokens as above. Preserve `ignore:` patterns and the +`coverage.status` block verbatim — Codecov only cares about the report path and +the source layout, both of which Kover preserves under `useJacoco(...)`. + +### `scripts/*.sh` + +Substitute task and path tokens. **Flag** any script that reads raw `.exec` +files (e.g. `build/jacoco/test.exec`) or globs `build/jacoco*` directories — +Kover does not expose them; the script either needs to switch to the XML report +under `build/reports/kover/` or be retired. + +## 6. KMP recipe (JVM target only) + +Per decision 5, only the JVM target migrates. Non-JVM targets are out of scope. + +- Apply `id("org.jetbrains.kotlinx.kover")` (literal; the Gradle Kotlin DSL + `plugins { }` block does not accept the buildSrc `Kover.id` constant). Or + use `kmp-module`, which applies Kover automatically. +- Use Kover's default report task and XML: + - Task: `::koverXmlReport` + - XML: `/build/reports/kover/report.xml` + + When the `kover { reports { total { … } } }` block is the only report + configured (as in `kmp-module.gradle.kts:181-190`), Kover does **not** + generate a separate `koverXmlReport` task per target — the + `total` report aggregates every Kotlin variant the module declares, and + because Spine only migrates the JVM target the aggregate is JVM-shaped. + A `koverXmlReportJvm` task only exists when a named `variant("jvm") { … }` + block is added explicitly, which `kmp-module` does not do. +- Configuration block at module scope: + ```kotlin + kover { + useJacoco(version = "0.8.14") // matches `io.spine.dependency.test.Jacoco.version` + reports { + total { + xml { onCheck = true } + } + } + } + ``` + (`kmp-module.gradle.kts:181-190` already has the right shape.) +- CI / `.codecov.yml` use `koverXmlReport` and + `build/reports/kover/report.xml`, same as for a Kotlin-JVM module. + +## 7. Manual-review surfaces + +These show up during detection and translation. **Flag** them in the proposal +and ask the user to decide before applying: + +- **Custom `sourceDirectories` / `classDirectories`** on `jacocoTestReport` — + the `walkBottomUp` recipe used by `jacoco-*-jvm.gradle.kts`. Safe to drop for + standard Kotlin-JVM / KMP layouts; ask if the module is non-Kotlin or has + unusual source roots. +- **Custom `reports.xml.destination` / `outputLocation`** — Kover writes to a + fixed path; CI consumers must follow. +- **Custom `executionData` paths** — Kover manages exec data internally; flag + if anything else (e.g. a coverage uploader) reads them directly. +- **Indirect `jacoco.toolVersion`** — a Gradle property + (`gradle.properties`, `-PjacocoVersion=…`) or convention plugin pinning a + non-`Jacoco.version` engine. Decide which version `useJacoco(version = …)` + should match. +- **Multi-pipeline setups** where both vanilla JaCoCo and Kover are intentional + (e.g. publishing two different reports for two consumers). Per decision 4 the + default is to strip JaCoCo, but confirm. +- **`JacocoConfig.applyTo(rootProject)` in a consumer repo** — rewrite to + `KoverConfig.applyTo(rootProject)` (§3, *Strip JaCoCo*). The Kover helper + preserves the generated-code exclusion that `JacocoConfig` provided. Do + **not** simply delete the call — that would silently drop the exclusion and + cause generated code to appear as uncovered in reports. +- **Custom convention plugins** applying JaCoCo under a name other than + `jacoco-…` — will be missed by the script-plugin detection in §2. Inspect + any `buildSrc/src/main/kotlin/*.gradle.kts` that imports `jacoco`. +- **Non-JVM KMP targets** (decision 5 — out of scope). Surface them so the user + knows their coverage is not migrated. +- **`dependsOn("jacocoTestReport")` from Groovy or external sources** — the + translation table rewrites Kotlin-script references; Groovy or external + callers may still reach for the old task name. + +## 8. References + +- Kover Gradle plugin: +- Kover 0.7 migration guide: + +- Kover DSL reference (verify / reports / filters): + +- JaCoCo XML schema (engine, preserved under `useJacoco(...)`): + +- Spine convention sources: + - `buildSrc/src/main/kotlin/jvm-module.gradle.kts` (Kover applied at L54, + configured at L99) + - `buildSrc/src/main/kotlin/kmp-module.gradle.kts` (Kover applied at L74, + configured at L181) + - `buildSrc/src/main/kotlin/io/spine/dependency/test/Kover.kt` + - `buildSrc/src/main/kotlin/io/spine/dependency/test/Jacoco.kt` diff --git a/.agents/skills/review-docs/SKILL.md b/.agents/skills/review-docs/SKILL.md index d7cac33237..41cef810f6 100644 --- a/.agents/skills/review-docs/SKILL.md +++ b/.agents/skills/review-docs/SKILL.md @@ -37,9 +37,14 @@ The authoritative standards live in `.agents/`: - `**/*.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. + Apply the `AGENTS.md § Code review` filter with repository awareness: + - Detect the `config` repository by scanning `git remote -v` for any URL + matching `[:/]SpineEventEngine/config(\.git)?$`. + - In **`config` itself**, skip only `gradlew` and `gradlew.bat`; every other + config-distributed path is owned by this repo and stays in scope. + - In any **consumer repo**, skip the full config-distributed list. 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 @@ -69,6 +74,14 @@ The authoritative standards live in `.agents/`: `// 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. +- **No repository-internal references in API docs.** KDoc and Javadoc must + not mention `buildSrc/`, the `config` repository or its `config/buildSrc/`, + or any path under `.agents/` (task plans, skill rules, conventions, …). + These details are invisible to consumers of the published artifact and + rot quickly. Cross-repository parity notes and work-in-progress + justifications belong in `.agents/tasks/`, not in the API docs. A mention + in newly-added or modified KDoc/Javadoc is a Should-fix; summarise the + *outcome* in the doc instead. - **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 (`//`). @@ -97,13 +110,13 @@ The authoritative standards live in `.agents/`: - **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. + - **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 diff --git a/.agents/skills/review-docs/agents/openai.yaml b/.agents/skills/review-docs/agents/openai.yaml new file mode 100644 index 0000000000..672388c445 --- /dev/null +++ b/.agents/skills/review-docs/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Review Docs" + short_description: "Review KDoc and Markdown changes" + default_prompt: "Use $review-docs to review KDoc, Javadoc, Protobuf comments, and Markdown changes against Spine documentation conventions." diff --git a/.agents/skills/update-copyright/SKILL.md b/.agents/skills/update-copyright/SKILL.md index 6afc4c7cf2..604ba30dda 100644 --- a/.agents/skills/update-copyright/SKILL.md +++ b/.agents/skills/update-copyright/SKILL.md @@ -3,14 +3,22 @@ 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. + Automatically apply to changed source files 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. +1. Scope: + - Automatic follow-up after edits: collect the source files modified by the + current change set and pass those paths explicitly. Do not run the command + without paths in the automatic path. If no changed source files remain + after filtering, skip the command. + - User-provided files/dirs: pass the requested paths explicitly. + - Repo-wide refresh: use no explicit paths only when the user directly asks + to update all tracked source files. +2. Repo-wide refresh → 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 index 246dd647f7..a56f8ab810 100644 --- a/.agents/skills/update-copyright/agents/openai.yaml +++ b/.agents/skills/update-copyright/agents/openai.yaml @@ -1,4 +1,4 @@ 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." + default_prompt: "Use $update-copyright to refresh copyright headers for changed source files from the IntelliJ IDEA copyright profile." diff --git a/.agents/skills/version-bumped/SKILL.md b/.agents/skills/version-bumped/SKILL.md index 86ca53df04..8f71383235 100644 --- a/.agents/skills/version-bumped/SKILL.md +++ b/.agents/skills/version-bumped/SKILL.md @@ -2,7 +2,7 @@ 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: + the base ref; run the `bump-version` skill 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 @@ -15,7 +15,7 @@ description: > 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. +and, if it fails, runs the `bump-version` skill and re-runs to confirm. The same logic is enforced as a hook (`.agents/scripts/publish-version-gate.sh`) that fires before any @@ -34,7 +34,7 @@ the version must advance. - Automatically: as the final step of any skill that may change files on the branch. -- Manually (`/version-bumped`): before running `./gradlew build` or +- 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. @@ -58,26 +58,26 @@ the version must advance. - **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` + `version.gradle.kts`). Do **not** run the `bump-version` skill automatically. Surface the script's stderr to the user and stop. -3. On exit 1, invoke `/bump-version` to perform the actual bump. That +3. On exit 1, run the `bump-version` skill 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 +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` +## 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 +`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 +`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` diff --git a/.agents/skills/version-bumped/agents/openai.yaml b/.agents/skills/version-bumped/agents/openai.yaml new file mode 100644 index 0000000000..7b39a0fedc --- /dev/null +++ b/.agents/skills/version-bumped/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Version Bumped" + short_description: "Ensure branch version was bumped" + default_prompt: "Use $version-bumped to verify the branch has advanced version.gradle.kts versus the base ref, auto-recovering through $bump-version when applicable." 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/buildsrc-gradle-review-findings.md b/.agents/tasks/buildsrc-gradle-review-findings.md new file mode 100644 index 0000000000..8890e49419 --- /dev/null +++ b/.agents/tasks/buildsrc-gradle-review-findings.md @@ -0,0 +1,303 @@ +--- +slug: buildsrc-gradle-review-findings +branch: gradle-review-skill +owner: claude +status: draft +started: 2026-05-29 +related-memories: [] +--- + +## Goal + +Apply the findings of the `/gradle-review` run against all sources +under `buildSrc/` in `config` (2026-05-29). The review found three +categories of issues: Spine mandate violations (`group = "spine"` / +`description`), Gradle correctness issues (`Provider.get()` at +configuration time, eager `Configuration`/`FileCollection` APIs that +discard task wiring), and a layer of Should-fix items around +cacheability annotations, `@PathSensitivity`, and lazy task +realisation. The work is large enough that it ships as a separate PR +from the `gradle-review-skill` branch. + +## Context + +- Review transcript: ran `/gradle-review` on the full `buildSrc/` + tree on 2026-05-29 in the `gradle-review-skill` branch. Verdict: + `REQUEST CHANGES`. +- Authoritative rules: + - [`.agents/skills/gradle-review/spine-task-conventions.md`](../skills/gradle-review/spine-task-conventions.md). + - [`.agents/skills/gradle-review/practices/tasks.md`](../skills/gradle-review/practices/tasks.md) + (ingested from the Gradle "Best practices for tasks" page). +- `SpineTaskGroup` constant already exists at + `buildSrc/src/main/kotlin/io/spine/gradle/SpineTaskGroup.kt` and is + the recommended replacement for the bare string `"spine"` (see + [`.agents/tasks/spine-task-group-constant.md`](spine-task-group-constant.md)). +- Pre-flight ripgrep confirmed: **0 hits** for `tasks.create(`, + `@CacheableTask`, `@DisableCachingByDefault`, `@UntrackedTask`, + `@PathSensitivity`, and the various `@Input*` / `@Output*` + annotations across `buildSrc/src/main/kotlin/**`. + +## Plan + +### A. Spine mandate — `group = SpineTaskGroup.name` (Must fix) + +The string `"spine"` does not appear as a task group anywhere in +`buildSrc/`. Two sub-patterns to fix: + +**A.1 — Tasks that set `group` to a non-Spine value.** + +- [x] `buildSrc/src/main/kotlin/io/spine/gradle/testing/Tasks.kt:82, 96` + — `FastTest` / `SlowTest`: replace `group = "Verification"` + with `group = SpineTaskGroup.name`. +- [x] `buildSrc/src/main/kotlin/io/spine/gradle/dart/task/DartTasks.kt:111-114` + — drop the `Group` object (`"Dart/Build"`, `"Dart/Publish"`). +- [x] `buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/JsTasks.kt:111-117` + — drop the `Group` object (`"JavaScript/Assemble"`, + `"JavaScript/Check"`, `"JavaScript/Clean"`, `"JavaScript/Build"`, + `"JavaScript/Publish"`). +- [x] Update every `Group.*` consumer to set + `group = SpineTaskGroup.name` instead: + `Webpack.kt:104`, `Check.kt:100, 129, 164, 189`, + `Assemble.kt:108, 133, 161, 188`, `Publish.kt:93, 116, 156, 187`, + `Clean.kt:90, 117`, `IntegrationTest.kt:90, 121`, + `LicenseReport.kt:80`, `dart/task/Build.kt:101, 128, 150`, + `dart/task/Publish.kt:95, 131, 163`. + +**A.2 — Tasks that set neither `group` nor `description`.** +For each, add `group = SpineTaskGroup.name` and an imperative +`description` (no trailing period): + +- [x] `buildSrc/src/main/kotlin/io/spine/gradle/javadoc/ExcludeInternalDoclet.kt:94` + (`tasks.register(taskName, Javadoc::class.java)`). +- [x] `buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt:58` + (`tasks.register(taskName, CheckVersionIncrement::class.java)`). +- [x] `buildSrc/src/main/kotlin/io/spine/gradle/github/pages/UpdateGitHubPages.kt:121, 149, 165, 183` + (`updateGitHubPages`, `copyJavadocDocs`, `copyHtmlDocs`, + `updatePagesTask`). +- [x] `buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/JacocoConfig.kt:165, 196` + (`jacocoRootReport`, `copyReports`). **Superseded by Kover-only + migration**: this file is deprecated; do not invest in micro-rewrites. + See `.agents/skills/raise-coverage/references/migrate-to-kover.md`. +- [x] `buildSrc/src/main/kotlin/io/spine/gradle/report/license/LicenseReporter.kt:111` + (`mergeAllLicenseReports`). +- [x] `buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomGenerator.kt:85` + (`generatePom`). +- [x] `buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt:235, 249, 261, 273` + (`sourcesJar`, `protoJar`, `testJar`, `javadocJar` via + `getOrCreate`). +- [x] `buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.kt:100, 121, 136` + (three registrations). +- [x] `buildSrc/src/main/kotlin/write-manifest.gradle.kts:106` + (`exposeManifestForTests`). +- [x] `buildSrc/src/main/kotlin/config-tester.gradle.kts:53` + (the script's local `clean`). +- [x] `buildSrc/src/main/kotlin/jvm-module.gradle.kts:152` + (`cleanGenerated`). + +**A.3 — Additional Spine-owned registrations uncovered during +review of Section A.** Not listed in the original `/gradle-review` +report but covered by the same mandate. All addressed in the same PR. + +- [x] `buildSrc/src/main/kotlin/DokkaExts.kt:206` + (`htmlDocsJar` via `getOrCreate`). +- [x] `buildSrc/src/main/kotlin/io/spine/gradle/dart/task/IntegrationTest.kt:76` + (`DartTasks.integrationTest`). +- [x] `buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt:157` + (`getOrCreatePublishTask` — the root-aggregator `publish` task + created when the `maven-publish` plugin is absent). +- [x] `buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt:186` + (`registerCheckCredentialsTask` — both the `register` and the + `replace` code paths). + +### B. `Provider.get()` outside a task action (Must fix) + +Each of these forces evaluation during the configuration phase, +breaking the configuration cache and serialising work Gradle would +otherwise run in parallel. + +- [x] `buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts:58-72` — + rewrite the `tasks.getting(JacocoReport::class) { ... }` block + to `tasks.named("jacocoTestReport") { ... }`, + remove the `project.layout.buildDirectory.get().asFile.absolutePath` + call, and replace the eager `walkBottomUp().toSet()` with + a lazy `DirectoryProperty`/`Provider` chain. Note + that the current code silently produces an empty set on a + clean build because `build/classes/kotlin/jvm/` does not yet + exist — that correctness bug goes away with the lazy form. + **Superseded by Kover-only migration**: this file is deprecated; + do not invest in micro-rewrites. See + `.agents/skills/raise-coverage/references/migrate-to-kover.md`. +- [ ] `buildSrc/src/main/kotlin/DokkaExts.kt:62` — change + `dokkaHtmlOutput(): File` to return `Provider` (or + a `DirectoryProperty`); update its two call sites. +- [ ] `buildSrc/src/main/kotlin/io/spine/gradle/ProjectExtensions.kt:105-106` + — `val Project.buildDirectory: File`: same pattern. Either + delete the helper (it just shells out to + `layout.buildDirectory.get().asFile`) or change it to return + `Provider`. Audit callers before deciding. +- [ ] `buildSrc/src/main/kotlin/io/spine/gradle/report/license/LicenseReporter.kt:84` + — `project.layout.buildDirectory.dir(Paths.relativePath).get().asFile`: + compose with `map` and consume inside the action. +- [x] `buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/JacocoConfig.kt:98-99` + — same pattern with the `reportsDirSuffix` directory. + **Superseded by Kover-only migration**: this file is deprecated; + do not invest in micro-rewrites. See + `.agents/skills/raise-coverage/references/migrate-to-kover.md`. +- [ ] `buildSrc/src/main/kotlin/DependencyResolution.kt:146` — + `named(configurationName).get().exclude(...)`: rewrite as + `named(configurationName) { exclude(...) }`. + +### C. Eager `FileCollection` / `Configuration` APIs (Must fix) + +These resolve configurations during configuration *and* discard +implicit task-dependency wiring — the wrong-outputs failure mode the +upstream rule warns about. + +- [ ] `buildSrc/src/main/kotlin/io/spine/gradle/javadoc/ExcludeInternalDoclet.kt:109` + — `docletpath = excludeInternalDoclet.files.toList()`: route + via a `Provider>` from `excludeInternalDoclet.elements`, + resolved inside the `Javadoc` task action. +- [x] `buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/CodebaseFilter.kt:65` + — `it.classesDirs.files.stream()`: switch to + `it.classesDirs.elements` and a lazy stream/iterator. + **Superseded by Kover-only migration**: this file is deprecated; + do not invest in micro-rewrites. See + `.agents/skills/raise-coverage/references/migrate-to-kover.md`. + +### D. Plugin task classes — caching annotations (Should fix) + +None of the three custom `DefaultTask` subclasses are annotated. +Document the contract: + +- [ ] `buildSrc/src/main/kotlin/io/spine/gradle/RunGradle.kt:48` + — add + `@DisableCachingByDefault(because = "Runs an external Gradle build whose outputs are not tracked")`. +- [ ] `buildSrc/src/main/kotlin/io/spine/gradle/publish/CheckVersionIncrement.kt:45` + — add + `@DisableCachingByDefault(because = "Performs network I/O against a Maven repository")`. +- [ ] `buildSrc/src/main/kotlin/io/spine/gradle/docs/UpdatePluginVersion.kt:56` + — add + `@DisableCachingByDefault(because = "Rewrites build scripts in place without declared outputs")`. + +### E. `@PathSensitivity` (Should fix) + +- [ ] `buildSrc/src/main/kotlin/io/spine/gradle/docs/UpdatePluginVersion.kt:58-59` + — add `@get:PathSensitive(PathSensitivity.RELATIVE)` on the + `directory` `DirectoryProperty`. The task only cares about + file names matching `build.gradle.kts` within the tree. + +### F. Eager realisation in convention code (Should fix) + +Replace eager APIs with their lazy siblings where one exists: + +- [ ] `buildSrc/src/main/kotlin/uber-jar-module.gradle.kts:73` — + `tasks.getting` → `tasks.named("publishFatJarPublicationToMavenLocal") { ... }`. +- [x] `buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts:58` — + `tasks.getting(JacocoReport::class)` → + `tasks.named("jacocoTestReport") { ... }` (folded + into B above). + **Superseded by Kover-only migration**: this file is deprecated; + do not invest in micro-rewrites. See + `.agents/skills/raise-coverage/references/migrate-to-kover.md`. +- [ ] `buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt:60` + — `tasks.getByName("check").dependsOn(this)` → + `tasks.named("check") { dependsOn(this@register) }`. +- [ ] `buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomGenerator.kt:95, 99` + — `tasks.findByName("assemble")!!` / + `tasks.findByName("build")!!` → + `tasks.named("assemble") { ... }` / `tasks.named("build") { ... }`. +- [ ] `buildSrc/src/main/kotlin/io/spine/gradle/javadoc/JavadocConfig.kt:41, 46` + — `getByName(named) as Javadoc` → `tasks.named(named)`; + ripple through callers (`ExcludeInternalDoclet.kt:93`, + `JavadocConfig.kt:73`). +- [ ] `buildSrc/src/main/kotlin/dokka-setup.gradle.kts:51` — replace + `val kspKotlin = tasks.findByName("kspKotlin")` inside + `afterEvaluate { ... }` with a + `tasks.matching { it.name == "kspKotlin" }.configureEach { ... }` + block, or rely on `tasks.named("kspKotlin").orNull`. + +### G. `dependsOn` where input/output wiring expresses the link (Should fix) + +- [ ] `buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt:273-278` + — `javadocJar()`: replace + `from(layout.buildDirectory.dir("dokka/javadoc"))` + + `dependsOn("dokkaGeneratePublicationJavadoc")` with + `from(tasks.named("dokkaGeneratePublicationJavadoc").map { it.outputs.files })`. +- [ ] `buildSrc/src/main/kotlin/DokkaExts.kt:206-213` — + `htmlDocsJar()`: same pattern; wire + `from(tasks.dokkaHtmlTask().map { it.outputs.files })` and + remove the explicit `dependsOn(dokkaTask)`. +- [x] `buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/JacocoConfig.kt:196-203` + — drop the trailing `dependsOn(projects.map { ... })` once + `everyExecData` is verified to be a Provider-typed chain that + carries producer dependencies. + **Superseded by Kover-only migration**: this file is deprecated; + do not invest in micro-rewrites. See + `.agents/skills/raise-coverage/references/migrate-to-kover.md`. +- [ ] `buildSrc/src/main/kotlin/io/spine/gradle/report/license/LicenseReporter.kt:117-118, 123` + — the explicit `consolidationTask.dependsOn(perProjectTask)` + and `perProjectTask.dependsOn(assembleTask)` should be + expressed via the merge task's `@InputFiles` on the per-project + report files. Refactor `mergeReports` to take a + `ListProperty` and let Gradle infer ordering. + +### H. Nits + +- [ ] **Trailing period in `description`** — strip from every Dart/JS + task helper (`Webpack.kt:103`, `Check.kt:99, 128, 163, 188`, + `Assemble.kt:107, 132, 160, 187`, `Publish.kt:92, 115, 155, 186`, + `Clean.kt:89, 116`, `IntegrationTest.kt:88, 120`, + `LicenseReport.kt:79`, `dart/task/Build.kt:100, 127, 149`, + `dart/task/Publish.kt:94, 130, 162`) and from + `testing/Tasks.kt:81, 95`. +- [ ] **KDoc back-link.** Add a KDoc link to the relevant rule from + [`.agents/skills/gradle-review/practices/tasks.md`](../skills/gradle-review/practices/tasks.md) + (or the upstream Gradle page) on each of `RunGradle.kt`, + `CheckVersionIncrement.kt`, `UpdatePluginVersion.kt`. +- [ ] **`project` access inside task actions** — `RunGradle.kt:142-180` + (`project.rootDir`, `project.gradle.taskGraph.hasTask(":clean")`, + `project.file(directory)`, `project.rootProject`), + `CheckVersionIncrement.kt:60-115` (`project.artifactPath()` and + friends), `PomGenerator.kt:85-93`, + `LicenseReporter.kt:120-122`. Capture the necessary values or + `Provider`s during configuration; pass them in via task + properties. +- [ ] **`@Internal lateinit var directory: String` in `RunGradle.kt:60-62`** + — should be a `DirectoryProperty` (or at least a + `Property`) so the task can participate in + configuration-cache serialisation. + +### Verification + +- [ ] Run `./gradlew clean build` against `config` and confirm it + passes without configuration-cache warnings. +- [ ] Re-run `/gradle-review` against `buildSrc/` and confirm + `APPROVE` (or `APPROVE WITH CHANGES` for residual Nits). +- [ ] Smoke-test downstream consumers (`base`, `base-types`, + `core-java`) via the `buildDependants` task in + `config-tester.gradle.kts`. + +## Decisions + +- **Scope.** All three findings categories (Must fix, Should fix, + Nits) are in scope. The volume justifies a dedicated PR. +- **Sequencing.** Sections A and B-C are independent and can be done + in either order. Section G depends on the lazy-Provider rewrites + in B for `DokkaExts.kt`. Run the verification step after every + section to keep the PR bisectable. +- **Out of scope.** `io.spine.dependency.*` files (owned by the + `dependency-audit` skill) and `gradlew` / `gradlew.bat` are + excluded from this task. + +## Log + +- 2026-05-29 — drafted from the `/gradle-review` run on the full + `buildSrc/` tree. Branch `gradle-review-skill` carries the + document; execution lands in a separate PR. +- 2026-05-29 — Section A applied on branch + `address-gradle-review-01`. Five review rounds against the diff + surfaced four additional Spine-owned task registrations that the + original report missed (`htmlDocsJar`, dart `integrationTest`, the + root-aggregator `publish` task, and `checkCredentials`); all four + added to Section A.3 and addressed in the same PR. Sections B–H + remain pending. diff --git a/.agents/tasks/cross-agent-skill-best-practices.md b/.agents/tasks/cross-agent-skill-best-practices.md new file mode 100644 index 0000000000..4876f28c86 --- /dev/null +++ b/.agents/tasks/cross-agent-skill-best-practices.md @@ -0,0 +1,100 @@ +--- +slug: cross-agent-skill-best-practices +branch: codex/audit-skills-discoverability +owner: codex +status: draft +started: 2026-05-31 +--- + +## Goal + +Bring the repository skills in `.agents/skills/` closer to the shared skills +standard so they are easy to discover and execute across Codex, Claude, and +other compatible agents. Success means a new agent can identify the right skill +from metadata, load a short `SKILL.md`, follow agent-neutral instructions, and +delegate deterministic work to scripts or references where appropriate. + +## Context + +- Audit source: Claude skill authoring best practices.[^claude-best-practices] +- Current inventory: 16 skills, 16 `SKILL.md` files, and 16 + `agents/openai.yaml` files. +- Good baseline: skill directory names match frontmatter names, names use the + expected lowercase hyphenated form, all `SKILL.md` files are under the + 500-line guideline, and frontmatter descriptions are under 1024 characters. +- User direction: optimize for compatibility with Codex, Claude, and other AI + agents that support the skills standard, not for a single agent runtime. + +## Findings + +1. Some fragile deterministic workflows are still mostly prose instead of + scripts. + - `check-links` embeds site detection, binary preflight, Lychee download, + Hugo server lifecycle, reporting, and sentinel writing in `SKILL.md`. + - `dependency-update` asks the agent to parse Kotlin dependency files, + discover versions, compare versions, and edit files manually. + - Best-practice risk: high-cognitive-load procedures are harder for agents + to pick up reliably and should be moved behind deterministic entrypoints + where practical. + +2. `raise-coverage` has a high-impact automatic path. + - The skill silently installs Kover when no coverage plugin is present. + - Best-practice risk: a request to add tests can mutate build configuration + without an explicit approval checkpoint. + - Cross-agent concern: different agents may interpret "silent install" + differently, so this should become an explicit policy decision. + +3. Long reference files need top-level contents. + - `raise-coverage/references/coverage-signals.md` is 181 lines. + - `raise-coverage/references/migrate-to-kover.md` is 352 lines. + - `gradle-review/practices/tasks.md` is 147 lines. + - Best-practice risk: reference material over 100 lines should be easier to + skim before an agent loads or follows a specific section. + +4. Some metadata and prompt surfaces are less portable than the rest. + - `raise-coverage/agents/openai.yaml` has a much longer `default_prompt` + than other skills. + - `writer/agents/openai.yaml` does not mention `$writer`, unlike the other + skill prompts. + - `raise-coverage/SKILL.md` still uses slash-command phrasing such as + `/raise-coverage` and `/version-bumped`, which is less portable across + agents. + +5. Evaluation evidence is missing. + - No eval or scenario files were found under `.agents/skills/`. + - Only `update-copyright` currently has script tests. + - Best-practice risk: the repo does not make it visible that skills were + tested on realistic examples, so future agents cannot distinguish + validated workflows from untried instructions. + +## Plan + +- [ ] Decide whether `raise-coverage` may silently install Kover, or whether all + build-configuration edits require explicit approval. +- [ ] Extract or introduce deterministic entrypoints for the highest-risk + procedural skills, starting with `check-links` and `dependency-update`. +- [ ] Add table-of-contents sections to reference files over 100 lines. +- [ ] Normalize cross-agent phrasing by removing slash-command assumptions and + keeping instructions skill-name based. +- [ ] Shorten unusually long `openai.yaml` default prompts while preserving + discoverability for Codex. +- [ ] Decide whether to add lightweight skill scenarios or eval notes for the + major skills. +- [ ] Re-audit all skills against the Claude best-practices checklist and record + the result in this task log. + +## Open Decisions + +- Should `raise-coverage` require approval before any Kover installation, even + when no coverage plugin exists? +- Should `dependency-update` get a real implementation script now, or should the + first pass only split parsing/versioning rules into references? +- What is the desired minimum evaluation artifact: short scenario files, + executable tests, or both? + +## Log + +- 2026-05-31: Drafted from the cross-agent skills best-practices audit. Awaiting + maintainer review before changes. + +[^claude-best-practices]: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices diff --git a/.agents/tasks/enforce-max-line-length.md b/.agents/tasks/enforce-max-line-length.md new file mode 100644 index 0000000000..3cad1c6962 --- /dev/null +++ b/.agents/tasks/enforce-max-line-length.md @@ -0,0 +1,279 @@ +--- +slug: enforce-max-line-length +branch: address-gradle-review-01 +owner: claude +status: draft +started: 2026-05-29 +related-memories: [] +--- + +## Goal + +Extend the agent-facing instructions and skills under `.agents/` so +that detekt's `MaxLineLength` rule +(`buildSrc/quality/detekt-config.yml:19-21`, +`maxLineLength: 100`, `excludeCommentStatements: true`) is honoured at +author time and surfaced at review time, instead of being discovered +late by CI on GitHub. + +Severity by file type: + +- **Detekt-enforced → Must fix** — non-comment lines in `.kt` / `.kts` + over the configured limit. These break `./gradlew build`. +- **Repo policy → Should fix** — KDoc / Javadoc body lines in any + source extension; `.java` lines; `.proto` lines; `.md` lines + (incl. `README.md`, `docs/**`, `.agents/**`). Detekt does not flag + these; the reviewer skills do. + +## Context + +CI and local builds repeatedly fail on detekt's `MaxLineLength` rule. +The user finds the late discovery — especially on GitHub — annoying. +None of the current agent instructions or skills name the rule, so +agents write code that breaks the build, then have to retry. + +### Framing + +The numeric threshold is a configuration parameter, not a constant. + +**Author-time behaviour**: agents read `MaxLineLength.maxLineLength` +from `buildSrc/quality/detekt-config.yml` once per session and treat +the value as a session-local constant. This is workable; re-reading +the YAML for every line of output is not. + +**Guidance text**: the new sections never bake the literal number +into `.agents/` prose. They reference the rule name and the file +path. If the threshold changes, the agent's session-start lookup +picks up the new value with no doc edit. + +**Review-time behaviour**: when a reviewer surfaces a finding, the +report cites the actual value (`"line 47 is 108 chars (limit 100, +from buildSrc/quality/detekt-config.yml)"`). The number lands in the +report, not in the rule. + +### KDoc handling (empirically verified) + +`excludeCommentStatements: true` excludes lines whose statement is a +comment — single-line `//`, trailing `//`, and KDoc body lines. The +exclusion of KDoc bodies is confirmed by +`buildSrc/src/main/kotlin/detekt-code-analysis.gradle.kts:52`, a +115-character KDoc body line that ships in the codebase and passes +the detekt build today. KDoc body lines are therefore Should-fix +repo policy, not Must-fix. + +### Splitting / restructure rules (confirmed with user) + +- String literals (including URLs inside strings) split at a + meaningful boundary into ≥ 2 `+`-concatenated pieces — never + truncated. +- Long imports: prefer an import alias + (`import a.b.c.LongName as Short`). If unavailable, a + `@file:Suppress("MaxLineLength")` is acceptable. +- Other unbreakable tokens (`[name][some.long.FQN]` in KDoc; long + generated identifier): prefer restructure (intermediate `val`, + reference-style Markdown link, alias). When no restructure is + reasonable, use `@Suppress("MaxLineLength")` on the declaration + with a brief `// Reason: …` comment. Use `@file:Suppress` only for + file-scope cases (e.g., a long import that cannot be aliased). + +### Scope clarifications + +- **Generated sources excluded**: do not flag lines under + `**/generated/**` or `**/generated-proto/**` — these are the paths + Spine's `buildSrc/quality/checkstyle.xml:35-42` and + `buildSrc/quality/pmd.xml:36-37` already exclude from the other + static-analysis runs. +- **Reading context vs. reporting scope.** Reviewers continue to read + each affected file fully (existing `kotlin-review` rule at + `.agents/skills/kotlin-review/SKILL.md:31-32`). They only *report* + line-length findings on lines the diff touched + (`git diff -U0 ...HEAD`). Pre-existing long lines are not + flagged. The two rules co-exist: read all, report changed. +- **`module.gradle.kts` carve-out**: per `AGENTS.md § Code review`, + in a consumer repo `buildSrc/src/main/kotlin/module.gradle.kts` is + in scope for the reviewers; it follows the same Must-fix rule as + any other `.kts`. +- **YAML lookup is from `HEAD`, not the base ref.** Long-lived + branches sometimes change `detekt-config.yml` mid-branch; reviewers + always re-read the value from the working tree, so the rule matches + what `./gradlew build` will see. +- **YAML missing is a hard error.** If + `buildSrc/quality/detekt-config.yml` is absent or lacks + `MaxLineLength.maxLineLength`, the reviewer reports a Must-fix + asking the user to restore the config rather than silently + inventing a number. + +## Plan + +Six `.agents/` Markdown files. No code or build changes. New lines +wrap at the configured limit. + +### 1. `.agents/coding-guidelines.md` + +- [ ] Add a new top-level `## Line length` section, placed immediately + after the existing "Text formatting" section. The canonical + content lives here; other docs cross-reference this heading. + Cover: + - Source-of-truth lookup: read `MaxLineLength.maxLineLength` from + `buildSrc/quality/detekt-config.yml` once at session start. Never + write the literal number into the guideline. + - Severity split (detekt-enforced vs. repo policy) per Context above. + - String-literal strategy with a small example whose split is at a + URL path boundary, e.g. + + ```kotlin + val ref = "https://github.com/SpineEventEngine/config/blob/master/" + + "buildSrc/quality/detekt-config.yml" + ``` + + This covers the URL-splitting case the user called out; the + existing `JacocoConfig.kt:122-125` pattern splits prose, not a + URL, and is not a sufficient teacher on its own. + - Unbreakable-token rules: import alias, restructure, then + `@Suppress` placement (on the declaration; `@file:Suppress` for + file-scope). + - Scope exclusions: generated sources; changed lines only. + +### 2. `.agents/documentation-guidelines.md` + +- [ ] Append one bullet to "Commenting guidelines": + + > Wrap KDoc / Javadoc body lines and Markdown body lines at the + > limit defined in `buildSrc/quality/detekt-config.yml` + > (`MaxLineLength.maxLineLength`). See + > `coding-guidelines.md § Line length` for the splitting strategy. + + Single sentence; no duplication of the canonical section. + +### 3. `.agents/quick-reference-card.md` + +- [ ] Rewrap the existing 135-char line 3 so the card itself respects + the rule it now advertises. +- [ ] Append one line (plain text, no decorative emoji — the rest of + the card uses 🚫 for a hard prohibition only, and line-length + guidance isn't in that category): + + > At session start, read `MaxLineLength.maxLineLength` from + > `buildSrc/quality/detekt-config.yml` and wrap new lines under it. + > See `coding-guidelines.md § Line length`. + +### 4. `.agents/skills/kotlin-review/SKILL.md` + +- [ ] In "Review procedure" step 3 (the coding-guidelines checklist), + append: + + > Line length (`MaxLineLength`). The reviewer reads the limit from + > `buildSrc/quality/detekt-config.yml` and applies it only to lines + > the diff touched. Non-comment `.kt` / `.kts` lines over the limit + > are **Must fix** (detekt breaks the build; + > `excludeCommentStatements: true` exempts KDoc bodies from the + > build break). KDoc bodies in `.kt` / `.kts`, and any `.java` line + > over the limit, are **Should fix**. For changed lines inside a + > string literal the fix is splitting into ≥ 2 `+`-concatenated + > pieces; otherwise follow `coding-guidelines.md § Line length`. + +- [ ] Update "Output format" correspondingly: add the bucket entries + but keep the existing Must / Should / Nits semantics unchanged. + +### 5. `.agents/skills/review-docs/SKILL.md` + +- [ ] Insert into "Checks → A. KDoc / Javadoc inside sources": + + > **Line length.** KDoc / Javadoc body lines wrap at the limit from + > `buildSrc/quality/detekt-config.yml`. Long body lines are + > **Should fix**; code lines around the comment, if also too long, + > are owned by `kotlin-review`. + +- [ ] Insert into "Checks → B. Markdown docs": + + > **Line length.** Body lines in `.md` — including `README.md`, + > `docs/**`, and `.agents/**` (this expands the skill's prior `.md` + > scope explicitly) — wrap at the configured limit. Long URLs go in + > reference-style footnote definitions. Long lines are + > **Should fix**. + +### 6. `.agents/skills/pre-pr/SKILL.md` + +- [ ] In the "Procedure" section, add a one-line pointer near the + existing reviewer-dispatch table (around + `.agents/skills/pre-pr/SKILL.md:104-106`): + + > Line-length findings on changed Kotlin / Java / Markdown lines + > are reported by the dispatched reviewers (`kotlin-review`, + > `review-docs`). pre-pr itself does not re-check. + + Documentation only — no logic change. Clarifies that the rule is + inherited via the existing dispatch and prevents future edits from + duplicating the check inside pre-pr. + +### Verification + +- [ ] Visually scan every edited file for the literal `100`. The + number should not appear in the new prose; only the rule name + and the YAML path should. +- [ ] Read the YAML, capture the value + (`LIMIT=$(awk '/maxLineLength:/ {print $2}' + buildSrc/quality/detekt-config.yml)`), and run + `awk -v n=$LIMIT 'length > n' `. `awk`'s + `length` counts bytes; for the ASCII prose introduced here that + matches characters, but a non-ASCII glyph in future edits would + miscount. Acceptable for this change. +- [ ] Sanity-check cross-references: every `coding-guidelines.md § + Line length` link resolves to the new top-level section heading. +- [ ] Spot test the author behaviour. In a fresh session, ask the + agent to write a long Kotlin string literal containing a URL; + confirm the result splits with `+` at a URL path boundary and + preserves every character. +- [ ] Spot test the reviewer behaviour. Synthesize a diff with: one + non-comment `.kt` line over the limit (expect Must fix); one + KDoc body line over the limit (expect Should fix); one `.java` + line over the limit (expect Should fix); one `.md` body line + over the limit (expect Should fix). Run `kotlin-review` and + `review-docs` and confirm bucketing. +- [ ] Confirm the missing-YAML behaviour: temporarily move + `buildSrc/quality/detekt-config.yml` aside, run a reviewer over + a synthetic diff, confirm it reports a **Must fix** asking the + user to restore the config (not a silent fallback). + +## Out of scope + +- `buildSrc/quality/detekt-config.yml` — unchanged. +- `writer/SKILL.md` and `java-to-kotlin/SKILL.md` — they author, they + don't enforce. The canonical rule in `coding-guidelines.md` reaches + them by reference. +- `gradle-review/SKILL.md` — `.kts` files are reviewed by + `kotlin-review` (via pre-pr's `code` dispatch). Adding a second + owner would double-report; defer to `kotlin-review § Line length`. +- `update-copyright/SKILL.md` — if a header rewrite produces a long + line, the reviewer will catch it; no skill-local rule. +- `memory/MEMORY.md` and `_TOC.md` — the rule is durable team policy + belonging in `.agents/`, indexed via the natural section heading. +- Rewrap of pre-existing over-length lines outside the diff (e.g., + `java-to-kotlin/SKILL.md:24,25,40,42`) — separate cleanup task, not + blocked by this plan. + +## Decisions + +- **KDoc severity**. Should-fix, not Must-fix. Empirically verified + by `buildSrc/src/main/kotlin/detekt-code-analysis.gradle.kts:52` + (115-char KDoc body line that ships and builds clean). +- **`gradle-review` not edited**. `.kts` files flow through + `kotlin-review` already (via pre-pr's `code` dispatch); a second + owner in `gradle-review` would cause double-reports for the same + finding. The trade-off is that manual `/gradle-review` runs without + a paired `/kotlin-review` will not surface line-length findings on + `.kts` files; users running only `gradle-review` are looking for + Gradle conventions, not detekt rules, so the gap is acceptable. +- **YAML lookup at session start, not per line**. Re-reading the YAML + for every line of output is impractical; the agent caches the value + as a session-local constant. Documentation never bakes the literal. +- **Missing YAML is Must-fix, not informational**. Avoids silent + fallback drift. + +## Log + +- 2026-05-29 — drafted in this session; plan revised twice to address + findings from two review rounds (KDoc empirics, generated-source + globs, `## Line length` heading placement, `gradle-review` → + `pre-pr` swap, YAML-missing severity, verification cleanup). + Awaiting approval. diff --git a/.agents/tasks/gradle-caching-plan.md b/.agents/tasks/gradle-caching-plan.md new file mode 100644 index 0000000000..efc3859ff2 --- /dev/null +++ b/.agents/tasks/gradle-caching-plan.md @@ -0,0 +1,200 @@ +--- +slug: gradle-caching-plan +branch: gradle-review-skill +owner: claude +status: draft +started: 2026-05-29 +--- + +# Plan: Speed Up Builds via Gradle Caching (org-wide, through `config`) + +> Implementation plan for Claude Code operating in the **`SpineEventEngine/config`** repository. +> Follow the repo's existing conventions in `CLAUDE.md` / `.agents/` (commit style, copyright +> headers, Kotlin guidelines, allowed commands). Make minimal diffs and land each phase as its +> own PR. + +## Purpose + +Make CI and local builds across the Spine organization faster by enabling **every free Gradle +caching layer**. Because `config` is the shared submodule pulled into every Spine repository, +changes here propagate org-wide — no per-repo edits required. + +## Why this work belongs in `config` + +`config` is added to each Spine project as a Git submodule, and `./config/pull` copies shared +files into the consuming project. Two of those files are exactly the levers we need: + +- **Root `gradle.properties`** — *overwritten* into each consuming repo on every `pull`. This is + the single source of truth for Gradle build flags. +- **`.github-workflows/`** — its workflow scripts are *merged into* each repo's + `.github/workflows/` on `pull`. This is where the CI definitions that run in every repo live. + (Per the repo README, these workflows intentionally do **not** run for `config` itself, so they + live under `.github-workflows/` rather than `.github/workflows/`.) + +Editing these here, then bumping the submodule + running `./config/pull` in a consuming repo, is +how the change reaches the whole org. + +## Goal + +Enable, in order of safety/ROI: + +1. **Dependency cache** — downloaded dependencies + wrapper distributions. +2. **Local build cache** — task outputs (`caches/build-cache-1`), persisted across CI runs so cold + CI builds skip unchanged work. +3. **Configuration cache** — skip Gradle's configuration phase on repeat runs (gated; higher risk). + +**Non-goal (out of scope here):** a *remote* build cache (Develocity or a self-hosted cache node). +That is the only layer that shares task outputs *across* repositories and across machines, but it +requires infrastructure (a reachable cache node + credentials, or Develocity) and is not a +config-only change. It is captured as a future phase, not to be implemented now. + +## Mental model (so changes are made for the right reasons) + +- The **dependency cache** speeds up *resolution/download*; it does not reuse build work. +- The **build cache** reuses *task outputs*, keyed by a hash of their inputs. Gradle's up-to-date + checks already cover "same workspace, nothing changed," so the build cache only adds value from a + **cold/fresh state** with unchanged inputs. +- **CI is cold on every run** (fresh checkout), so the build cache is precisely what helps CI — + independent of team size or number of repos. +- `gradle/actions/setup-gradle` persists the Gradle User Home (deps, wrapper, **and** + `caches/build-cache-1`) via the GitHub Actions cache. By default it **writes** the cache only + from the **default branch**; other branches **read** the default branch's cache. So PR builds + reuse what `main`'s CI produced, without polluting the shared cache. + - Caveat: for `pull_request`-triggered runs, the cache scope is the PR merge ref and writes are + disabled by default (only re-runs of the same PR restore them). The read-from-`main` behavior + still applies. + +## Guardrails (do / don't) + +- ✅ **DO** edit the **root `gradle.properties`** in `config` for all Gradle flags. +- ⛔ **DON'T** add Gradle flags to individual consuming repos' `gradle.properties` — `./config/pull` + overwrites that file, so such edits are lost. `config` is the only correct place. +- ✅ **DO** edit workflow templates in **`.github-workflows/`** (and, if you also want `config`'s + own CI to benefit, `config`'s own `.github/workflows/`). +- ⛔ **DON'T** keep `actions/setup-java` with `cache: gradle` alongside `setup-gradle` — the two + caching mechanisms conflict; remove `cache: gradle` when adding `setup-gradle`. +- ⛔ **DON'T** create any remote cache server, add secrets, create accounts, or change repo + permissions. (Out of scope; infra/owner decisions.) +- ✅ Keep diffs minimal: don't reorder or delete existing properties/steps that are unrelated. +- ✅ Land each phase as a **separate commit/PR** and validate before moving on. + +## Tasks + +### Phase 0 — Inventory (no changes) + +1. Read the root `gradle.properties`; record which `org.gradle.*` flags already exist (caching, + parallel, configuration-cache, jvmargs, etc.). +2. List `.github-workflows/`. For each workflow, locate the Java/Gradle setup steps and how Gradle + is invoked (`./gradlew ...`). Note any use of `actions/setup-java` with `cache: gradle`. +3. Check `config`'s own `.github/workflows/` separately (these run for `config` itself). +4. Read `gradle/wrapper/gradle-wrapper.properties` to determine the **Gradle version**. The stable + configuration-cache property names below assume Gradle **8.1+**; if older, adjust property names + and treat Phase 3 with extra caution. +5. Summarize findings before editing. + +### Phase 1 — Switch CI to `gradle/actions/setup-gradle` + +For each relevant workflow: + +- Remove `cache: gradle` from any `actions/setup-java` step. +- Add a `gradle/actions/setup-gradle@v6` step **after** Java setup and **before** any Gradle + invocation. (The action also configures init-scripts that apply to later `run: ./gradlew` steps.) +- Match the repo's existing action-pinning policy; current major versions available are + `actions/checkout@v6`, `actions/setup-java@v5`, `gradle/actions/setup-gradle@v6`. + +Reference shape (adapt to each workflow's actual jobs/matrix — do not blindly overwrite): + +```yaml +steps: + - uses: actions/checkout@v6 + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 17 # keep whatever the repo currently targets; no `cache: gradle` + - uses: gradle/actions/setup-gradle@v6 + - run: ./gradlew build +``` + +Notes: +- The default `enhanced` cache provider is **free for public repositories** (all Spine repos are + public). No `cache-provider` override needed unless a fully-MIT path is preferred + (`cache-provider: basic`). +- Leave the default write-on-default-branch-only behavior in place; it's the desired setup. + +### Phase 2 — Enable build cache + parallel in shared `gradle.properties` + +In the root `gradle.properties`, add (only if absent): + +```properties +org.gradle.caching=true +org.gradle.parallel=true +``` + +- `caching=true` enables the **local** build cache; combined with `setup-gradle` persisting + `caches/build-cache-1`, CI runs now reuse task outputs. +- `parallel=true` is generally safe but must be validated (see acceptance). + +### Phase 3 — Configuration cache (gated; higher risk) + +In the root `gradle.properties`, add: + +```properties +org.gradle.configuration-cache=true +org.gradle.configuration-cache.problems=warn +``` + +- Start in **warn** mode so configuration-cache-incompatible tasks **do not fail** the build. +- Spine relies on many custom Gradle plugins and code-generation tasks (Protobuf / model compiler + / etc.) that may not yet be configuration-cache compatible. Warn mode surfaces problems without + breaking builds. +- Where feasible, fix incompatibilities in **`buildSrc`** (the shared build logic). If problems are + extensive, **leave configuration cache in warn mode or defer Phase 3 entirely** — do **not** + switch to strict/fail mode until `buildDependants` is clean. +- (On Gradle < 8.1 the stable property differs; do not guess — check the wrapper version from + Phase 0 and use the matching property name, or skip this phase.) + +### Phase 4 — Remote build cache (FUTURE — do not implement now) + +Documented for completeness only. If pursued later: +- Configure `buildCache { remote(HttpBuildCache) { ... } }` (in `settings.gradle.kts` of consuming + projects, or centrally via `buildSrc`), pushing **only from CI**. +- Per Gradle's guidance, **disable the local build cache on CI** when a remote cache is available, + to keep GitHub Actions cache entries small. +- Requires a reachable cache node + credentials (or Develocity) and is an infrastructure decision — + not a config-only change. Stop and flag this to a human rather than implementing it. + +## Verification / acceptance criteria + +`config` ships `ConfigTester`, wired into `build.gradle.kts` as the `buildDependants` task, which +checks out and builds the dependant repos (`base`, `base-types`, `core-java`) against the **local** +`config`. Use it as the gate for every phase: + +```bash +./gradlew clean buildDependants # ~30+ minutes; builds base, base-types, core-java with local config +``` + +Acceptance for each phase: + +1. `buildDependants` **passes** with the change applied. +2. **Cache reuse is observable:** run a dependant build twice; the second run shows many tasks as + `FROM-CACHE` / `UP-TO-DATE`. +3. **CI evidence:** in a workflow run, the `setup-gradle` **Job Summary** reports cache entries + restored/saved; compare overall job wall-clock **before vs after**. +4. **Phase 3 specifically:** `buildDependants` completes with configuration cache enabled (warn mode + acceptable). Record any remaining configuration-cache problems in the PR description. + +## Rollout + +1. Land Phases 1–2 (and 3 if clean) as separate PRs in `config`. +2. Pilot in **one** consuming repo first (suggest `base`): bump the `config` submodule, run + `./config/pull` (this overwrites `gradle.properties` and merges `.github-workflows/` into + `.github/workflows/`), confirm CI is green and faster. +3. Propagate to the remaining repos once the pilot is validated. + +## References + +- `setup-gradle` docs: https://github.com/gradle/actions/blob/main/docs/setup-gradle.md +- Gradle Build Cache: https://docs.gradle.org/current/userguide/build_cache.html +- Gradle Configuration Cache: https://docs.gradle.org/current/userguide/configuration_cache.html +- `config` README (pull mechanism, `.github-workflows`, `ConfigTester`/`buildDependants`): + https://github.com/SpineEventEngine/config diff --git a/.agents/tasks/spine-task-group-constant.md b/.agents/tasks/spine-task-group-constant.md new file mode 100644 index 0000000000..24667819f9 --- /dev/null +++ b/.agents/tasks/spine-task-group-constant.md @@ -0,0 +1,104 @@ +--- +slug: spine-task-group-constant +branch: gradle-review-skill +owner: claude +status: in-progress +started: 2026-05-29 +related-memories: [] +--- + +## Goal + +Replace the bare string literal `"spine"` (the Gradle task group used +by every custom task in this organisation) with a shared constant in +two locations: + +1. **In `config`'s `buildSrc/`** — so all build files in `config` and + all consumer projects that apply `config` reference the same + symbol instead of repeating the literal. +2. **In `tool-base`** — so the production code of every Spine SDK + Gradle plugin references the same symbol when it registers or + configures tasks. + +Once both constants exist, `gradle-review` reports a remaining bare +literal `"spine"` as a Nit and recommends the relevant constant as +the replacement. + +## Context + +- The Spine convention "every custom task has `group = "spine"`" is + documented in + [`.agents/skills/gradle-review/spine-task-conventions.md`](../skills/gradle-review/spine-task-conventions.md). +- The `gradle-review` skill (see + [`../skills/gradle-review/SKILL.md`](../skills/gradle-review/SKILL.md)) + enforces the rule, and lists the constant migration as a Nit until + the symbol exists. +- Two separate codebases are involved because of dependency direction: + `buildSrc/` in `config` is on the build classpath of every consumer + project's `build.gradle.kts`, while `tool-base` is consumed at + runtime by SDK plugins. A single source-of-truth in `tool-base` and + a re-export from `buildSrc/` would couple the two — instead each + side declares its own constant and both keep the same value + (`"spine"`). The `gradle-review` skill cross-checks both. + +## Plan + +### A. `config/buildSrc` constant + +- [x] Add `object SpineTaskGroup { const val name = "spine" }` in + `buildSrc/src/main/kotlin/io/spine/gradle/SpineTaskGroup.kt` + with copyright header and KDoc referencing + `.agents/skills/gradle-review/spine-task-conventions.md`. +- [x] Migrate every `group = "spine"` usage in `buildSrc/**/*.kt` and + `buildSrc/**/*.gradle.kts` to the constant. (Verified by + `rg "group\s*=\s*\"spine\""` — no existing literals in + `buildSrc/`; the only `"spine"` occurrence there is the + unrelated artifact-prefix constant in `dependency/local/Base.kt`.) +- [x] Migrate every `group = "spine"` usage in the project's + `build.gradle.kts` and `settings.gradle.kts` (the constant is + visible from build files thanks to `buildSrc/`). (Verified — no + existing literals.) +- [x] Spot-check with `rg -n '"spine"' --type kotlin` (ripgrep's + built-in `kotlin` type covers both `*.kt` and `*.kts`; the + short alias `--type kt` is **not** recognised) — only the + constant declaration and unrelated occurrences (artifact + prefix in `Base.kt`, exclude rule for `spine-base` in + `DependencyResolution.kt`) remain. + +### B. `tool-base` constant + GitHub issue + +- [x] Open the tracking issue under `tool-base` — [tool-base#171][tool-base-171]. +- [ ] (Remaining migration is tracked by that issue, not this branch.) + +[tool-base-171]: https://github.com/SpineEventEngine/tool-base/issues/171 + +## Decisions + +- **Naming and shape.** `object SpineTaskGroup { const val name = "spine" }`. + Reference site reads `group = SpineTaskGroup.name`. Mirrors the + `JsTasks.Group.build` precedent already used inside `buildSrc/` and + leaves room for related constants later. Consistency with the + `tool-base` constant — once it exists — is more important than the + specific shape; the `tool-base` issue should adopt the same shape. +- **Location.** New file at + `buildSrc/src/main/kotlin/io/spine/gradle/SpineTaskGroup.kt`, + alongside `TaskName.kt` and other top-level Gradle helpers. + Visibility is `public` (default) so consumer `build.gradle.kts` + files can import the symbol. +- **KDoc link form.** Plain text path to + `.agents/skills/gradle-review/spine-task-conventions.md`; KDoc does + not resolve relative Markdown links in the IDE, and an absolute + GitHub URL would couple the source to a specific branch. + +## Log + +- 2026-05-29 — drafted alongside the `gradle-review` skill, awaiting + approval to start migration. +- 2026-05-29 — implemented `SpineTaskGroup` in `config/buildSrc` + (`io.spine.gradle.SpineTaskGroup`). Verified by ripgrep that no + bare `"spine"` task-group literals exist in `*.kt` or `*.gradle.kts` + under this repo, so the migration step in section A is a no-op + inside `config`. The constant is in place for new tasks added here + and for consumer repositories' build files. The `tool-base` + constant and its migration remain tracked under + [tool-base#171][tool-base-171]. diff --git a/.claude/commands/raise-coverage.md b/.claude/commands/raise-coverage.md new file mode 100644 index 0000000000..c428055ff5 --- /dev/null +++ b/.claude/commands/raise-coverage.md @@ -0,0 +1,26 @@ +--- +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. +argument-hint: "<:module | path | --triage>" +allowed-tools: Read, Edit, Write, Grep, Glob, Bash(./gradlew:*), Bash(git status:*), Bash(find:*), Bash(xmllint:*), Bash(python3:*) +--- + +Follow the `raise-coverage` skill exactly: + +- Skill: `.agents/skills/raise-coverage/SKILL.md` +- Target: $ARGUMENTS — a Gradle module (e.g. `:base`), a source path, or + `--triage` to only produce the ranked Kover gap report without generating tests. +- 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 `.agents/skills/raise-coverage/references/migrate-to-kover.md`. +- Order: localize gaps from Kover's JaCoCo-format XML → propose concrete test + cases and **wait for confirmation** → generate → re-run + `::koverXmlReport` to verify the gap closed. +- Honor `.agents/testing.md` and `.agents/coding-guidelines.md`. New tests are + always written in **Kotlin** (JUnit Jupiter structure + Kotest assertions), + regardless of whether the code under test is Kotlin or Java, with no + mocking framework — stubs only. Test class names use the **`Spec`** suffix + (e.g. `AbstractSourceFileSpec`). +- Target human-written `src/main` code only — never generated code, `examples`, + or existing tests. +- Never weaken a `.codecov.yml` target or add a mocking dependency to make a + check pass. Tests-only changes do not require a version bump. diff --git a/AGENTS.md b/AGENTS.md index f404b4ea4c..c2a8da50e9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,6 +55,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). 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/local/Validation.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt index 4a1f79c15f..cd4fc4a5c5 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt @@ -36,7 +36,7 @@ object Validation { /** * The version of the Validation library artifacts. */ - const val version = "2.0.0-SNAPSHOT.444" + const val version = "2.0.0-SNAPSHOT.445" /** * The last version of Validation compatible with ProtoData. diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/test/Jacoco.kt b/buildSrc/src/main/kotlin/io/spine/dependency/test/Jacoco.kt index 5f007ec518..53cd50176e 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/test/Jacoco.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/test/Jacoco.kt @@ -33,5 +33,5 @@ package io.spine.dependency.test */ @Suppress("ConstPropertyName") object Jacoco { - const val version = "0.8.13" + const val version = "0.8.14" } 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 2607c01755..bd80860e4c 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 2607c017551a643b32c29b338bacc96967261ef7 +Subproject commit bd80860e4c2bab50004e2f1720b8ee32e5c311c3 From 5e0395fd64d0f55db8f37a971c899357addaf1f1 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 1 Jun 2026 18:35:52 +0100 Subject: [PATCH 02/26] Bump version -> `2.0.0-SNAPSHOT.446` --- version.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle.kts b/version.gradle.kts index 1c7e1a39c4..bf51ca0fb3 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -27,4 +27,4 @@ /** * The version of the Validation library to publish. */ -val validationVersion by extra("2.0.0-SNAPSHOT.445") +val validationVersion by extra("2.0.0-SNAPSHOT.446") From cac8f492f279c5f1051046e9d2b0b662aac74784 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 1 Jun 2026 18:41:25 +0100 Subject: [PATCH 03/26] Bump local dependencies --- buildSrc/src/main/kotlin/io/spine/dependency/local/Base.kt | 4 ++-- .../src/main/kotlin/io/spine/dependency/local/Compiler.kt | 4 ++-- .../main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Base.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Base.kt index a6ca119701..a9509c1dc9 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Base.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Base.kt @@ -33,8 +33,8 @@ package io.spine.dependency.local */ @Suppress("ConstPropertyName", "unused") object Base { - const val version = "2.0.0-SNAPSHOT.390" - const val versionForBuildScript = "2.0.0-SNAPSHOT.390" + const val version = "2.0.0-SNAPSHOT.391" + const val versionForBuildScript = "2.0.0-SNAPSHOT.391" const val group = Spine.group private const val prefix = "spine" const val libModule = "$prefix-base" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt index 9f65ab2468..1b23ec6707 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt @@ -72,7 +72,7 @@ object Compiler : Dependency() { * The version of the Compiler dependencies. */ override val version: String - private const val fallbackVersion = "2.0.0-SNAPSHOT.044" + private const val fallbackVersion = "2.0.0-SNAPSHOT.046" /** * The distinct version of the Compiler used by other build tools. @@ -81,7 +81,7 @@ object Compiler : Dependency() { * transitive dependencies, this is the version used to build the project itself. */ val dogfoodingVersion: String - private const val fallbackDfVersion = "2.0.0-SNAPSHOT.044" + private const val fallbackDfVersion = "2.0.0-SNAPSHOT.046" /** * The artifact for the Compiler Gradle plugin. diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt index 4437b70dbf..461a5fda1e 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt @@ -46,12 +46,12 @@ object CoreJvmCompiler { /** * The version used in the build classpath. */ - const val dogfoodingVersion = "2.0.0-SNAPSHOT.067" + const val dogfoodingVersion = "2.0.0-SNAPSHOT.068" /** * The version to be used for integration tests. */ - const val version = "2.0.0-SNAPSHOT.067" + const val version = "2.0.0-SNAPSHOT.068" /** * The ID of the Gradle plugin. From 01cdac0c222a9065a740e25a5e35e6b41893b472 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 1 Jun 2026 18:41:31 +0100 Subject: [PATCH 04/26] Bump local dependencies --- .../src/main/kotlin/io/spine/dependency/local/ToolBase.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt index 12f3e0d274..999e93b05b 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt @@ -34,8 +34,8 @@ package io.spine.dependency.local @Suppress("ConstPropertyName", "unused") object ToolBase { const val group = Spine.toolsGroup - const val version = "2.0.0-SNAPSHOT.378" - const val dogfoodingVersion = "2.0.0-SNAPSHOT.378" + const val version = "2.0.0-SNAPSHOT.381" + const val dogfoodingVersion = "2.0.0-SNAPSHOT.381" const val lib = "$group:tool-base:$version" const val classicCodegen = "$group:classic-codegen:$version" From 21abeecc8e4b5b3b3d03e8c776150aa960ea9106 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 1 Jun 2026 18:42:45 +0100 Subject: [PATCH 05/26] Migrate to Kover --- build.gradle.kts | 6 +- .../gradle/report/coverage/CodebaseFilter.kt | 109 ------- .../gradle/report/coverage/FileExtension.kt | 62 ---- .../gradle/report/coverage/FileExtensions.kt | 175 ----------- .../gradle/report/coverage/FileFilter.kt | 58 ---- .../gradle/report/coverage/JacocoConfig.kt | 278 ------------------ .../gradle/report/coverage/PathMarker.kt | 63 ---- .../spine/gradle/report/coverage/TaskName.kt | 48 --- buildSrc/src/main/kotlin/module.gradle.kts | 2 +- 9 files changed, 4 insertions(+), 797 deletions(-) delete mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/CodebaseFilter.kt delete mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/FileExtension.kt delete mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/FileExtensions.kt delete mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/FileFilter.kt delete mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/JacocoConfig.kt delete mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/PathMarker.kt delete mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/TaskName.kt diff --git a/build.gradle.kts b/build.gradle.kts index 6aee557836..6456f7006c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -40,7 +40,7 @@ import io.spine.dependency.local.Validation 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 @@ -73,8 +73,8 @@ buildscript { } plugins { + base idea - jacoco `gradle-doctor` id("project-report") } @@ -139,6 +139,6 @@ allprojects { } } -JacocoConfig.applyTo(project) +KoverConfig.applyTo(rootProject) LicenseReporter.mergeAllReports(project) PomGenerator.applyTo(project) 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 deleted file mode 100644 index 8f47847caa..0000000000 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/CodebaseFilter.kt +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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 com.google.errorprone.annotations.CanIgnoreReturnValue -import io.spine.gradle.report.coverage.FileFilter.generatedOnly -import java.io.File -import org.gradle.api.Project -import org.gradle.api.file.ConfigurableFileTree -import org.gradle.api.file.FileTree -import org.gradle.api.tasks.SourceSetOutput - -/** - * Serves to distinguish the generated `.java` and `.kt` files (and the `.class` files - * compiled from them) from the human-created production code. - * - * 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, - private val outputDirs: Set -) { - - /** - * Returns the file tree containing the compiled `.class` files which were produced - * from the human-written production code. - * - * Such filtering excludes the output obtained from the generated sources. - */ - internal fun humanProducedCompiledFiles(): List { - log("Source dirs for the code coverage calculation:") - this.srcDirs.forEach { - log(" - $it") - } - - val generatedClassNames = generatedClassNames() - val humanProducedTree = outputDirs - .stream() - .flatMap { it.classesDirs.files.stream() } - .map { srcFile -> - log("Filtering out the generated classes for ${srcFile}.") - project.fileTree(srcFile).without(generatedClassNames) - }.toList() - return humanProducedTree - } - - private fun generatedClassNames(): List = - generatedOnly(srcDirs) - .filter { it.exists() && it.isDirectory } - .flatMap { root -> - root.walk() - .filter { !it.isDirectory } - .flatMap { it.classNamesIn(root) } - .toList() - } - - private fun log(message: String) { - project.logger.info(message) - } -} - -/** - * Excludes the elements which [Java compiled file names][File.asJavaCompiledClassName] - * are present among the passed [names]. - * - * Returns the same instance of `ConfigurableFileTree`, for call chaining. - */ -@CanIgnoreReturnValue -private fun ConfigurableFileTree.without(names: List): ConfigurableFileTree { - this.exclude { element -> - val className = element.file.asJavaCompiledClassName() - names.contains(className) - } - return this -} 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 deleted file mode 100644 index 7dad4345dd..0000000000 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/FileExtension.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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 - -/** - * 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) { - - /** - * Extension of a Java source file. - */ - JAVA_SOURCE(".java"), - - /** - * Extension of a Kotlin source file. - */ - KOTLIN_SOURCE(".kt"), - - /** - * Extension of a Java compiled file. - */ - COMPILED_CLASS(".class"); - - /** - * The number of symbols in the extension. - */ - val length: Int - get() = this.value.length -} 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 deleted file mode 100644 index c32f682928..0000000000 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/FileExtensions.kt +++ /dev/null @@ -1,175 +0,0 @@ -/* - * 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.gradle.report.coverage.FileExtension.COMPILED_CLASS -import io.spine.gradle.report.coverage.FileExtension.JAVA_SOURCE -import io.spine.gradle.report.coverage.FileExtension.KOTLIN_SOURCE -import io.spine.gradle.report.coverage.PathMarker.ANONYMOUS_CLASS -import io.spine.gradle.report.coverage.PathMarker.GENERATED -import io.spine.gradle.report.coverage.PathMarker.MAIN_OUTPUT_FOLDER -import java.io.File - -/** - * This file contains extension methods and properties for `java.io.File`. - */ - -/** - * The two-part extension used by `protoc-gen-kotlin` for proto-file-scoped Kotlin - * helpers (e.g., `FooProtoKt.proto.kt`). - */ -private const val PROTO_KOTLIN_SUFFIX = ".proto.kt" - -/** - * Suffix that the Kotlin compiler appends to the file name when generating the - * synthetic file class for top-level declarations. - */ -private const val KOTLIN_FILE_CLASS_SUFFIX = "Kt" - -/** - * Parses the name of a class from the absolute path of this file. - * - * Treats the fragment between the [precedingMarker] and [extension] as the value to look for. - * In case the fragment is located and it contains `/` symbols, they are treated - * as package delimiters and are replaced by `.` symbols before returning the value. - * - * 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 -): String? { - val index = this.absolutePath.lastIndexOf(precedingMarker.infix) - return if (index > 0) { - var inFolder = this.absolutePath.substring(index + precedingMarker.length) - if (inFolder.endsWith(extension.value)) { - inFolder = inFolder.substring(0, inFolder.length - extension.length) - inFolder.replace('/', '.') - } else { - null - } - } else { - null - } -} - -/** - * Attempts to parse the fully-qualified class name from the absolute path of this file, - * treating it as a path to a compiled `.class` file produced by either `javac` or `kotlinc`. - * - * 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)) { - className = className.split(ANONYMOUS_CLASS.infix)[0] - } - return className -} - -/** - * Returns the fully-qualified names of compiled JVM classes that originate from this - * source file, assuming [sourceRoot] is the source-set root under which the file was - * discovered. - * - * The shape of the returned list depends on the source file extension: - * - * - `.java` — a single FQN derived from the path relative to [sourceRoot]. - * - `.kt` — two FQNs: the declared file/class name, and the same name with `Kt` - * appended, which is the synthetic file class that Kotlin emits for top-level - * declarations. - * - `.proto.kt` — the two-part extension is stripped first; otherwise behaves - * like `.kt`. This is the convention used by `protoc-gen-kotlin` for files - * holding proto-file-scoped helpers. - * - Any other extension — an empty list. - * - * 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() - } - val relative = this.toRelativeString(sourceRoot) - 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.value) -> { - val base = relative.removeSuffix(KOTLIN_SOURCE.value).toFqn() - listOf(base, base + KOTLIN_FILE_CLASS_SUFFIX) - } - relative.endsWith(JAVA_SOURCE.value) -> - listOf(relative.removeSuffix(JAVA_SOURCE.value).toFqn()) - else -> emptyList() - } -} - -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 deleted file mode 100644 index 53793f466d..0000000000 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/FileFilter.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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 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 { - - /** - * Excludes the generated files from this file collection, leaving only those which were - * created by human beings. - */ - fun producedByHuman(files: Iterable): Iterable { - return files.filter { !it.isGenerated } - } - - /** - * Filters this file collection so that only generated files are present. - */ - fun generatedOnly(files: Iterable): Iterable { - return files.filter { it.isGenerated } - } -} 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 deleted file mode 100644 index 9a7e6e1f39..0000000000 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/JacocoConfig.kt +++ /dev/null @@ -1,278 +0,0 @@ -/* - * 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.gradle.SpineTaskGroup -import io.spine.gradle.applyPlugin -import io.spine.gradle.getTask -import io.spine.gradle.report.coverage.TaskName.check -import io.spine.gradle.report.coverage.TaskName.copyReports -import io.spine.gradle.report.coverage.TaskName.jacocoRootReport -import io.spine.gradle.report.coverage.TaskName.jacocoTestReport -import io.spine.gradle.sourceSets -import java.io.File -import java.util.* -import org.gradle.api.Project -import org.gradle.api.file.ConfigurableFileCollection -import org.gradle.api.file.SourceDirectorySet -import org.gradle.api.plugins.BasePlugin -import org.gradle.api.tasks.Copy -import org.gradle.api.tasks.SourceSetContainer -import org.gradle.api.tasks.SourceSetOutput -import org.gradle.api.tasks.TaskContainer -import org.gradle.api.tasks.TaskProvider -import org.gradle.api.tasks.testing.Test -import org.gradle.kotlin.dsl.get -import org.gradle.kotlin.dsl.the -import org.gradle.testing.jacoco.plugins.JacocoPlugin -import org.gradle.testing.jacoco.plugins.JacocoPluginExtension -import org.gradle.testing.jacoco.tasks.JacocoReport - -/** - * Configures JaCoCo plugin to produce `jacocoRootReport` task which accumulates - * the test coverage results from all subprojects in a multi-project Gradle build. - * - * Users must apply `jacoco` plugin to all the subprojects, for which the report aggregation - * is required. - * - * In a single-module Gradle project, this utility is NOT needed. Just a plain `jacoco` plugin - * applied to the project is sufficient. - * - * Therefore, in case this utility is applied to a single-module Gradle project, - * an `IllegalStateException` is thrown. - */ -@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, - private val projects: Iterable -) { - - companion object { - - /** - * A folder under the `buildDir` of the [rootProject] to which the reports will - * be copied when aggregating the coverage reports. - * - * If it does not exist, it will be created. - */ - private const val reportsDirSuffix = "subreports/jacoco/" - - /** - * Applies the JaCoCo plugin to the Gradle project. - * - * If the passed project has no subprojects, an `IllegalStateException` is thrown, - * telling that this utility should NOT be used. - * - * Registers `jacocoRootReport` task which aggregates all coverage reports - * from the subprojects. - */ - fun applyTo(project: Project) { - project.applyPlugin(BasePlugin::class.java) - project.gradle.projectsEvaluated { - val javaProjects: Iterable = eligibleProjects(project) - val reportsDir = project.rootProject.layout - .buildDirectory.dir(reportsDirSuffix).get().asFile - JacocoConfig( - project.rootProject, - reportsDir, - javaProjects - ).configure() - } - } - - /** - * For a multi-module Gradle project, returns those subprojects of the passed [project] - * which have JaCoCo plugin applied. - * - * Throws an exception in case this project has no subprojects. - */ - private fun eligibleProjects(project: Project): Iterable { - val projects: Iterable = - if (project.subprojects.isNotEmpty()) { - project.subprojects.filter { - it.pluginManager.hasPlugin(JacocoPlugin.PLUGIN_EXTENSION_NAME) - } - } else { - throw IllegalStateException( - "In a single-module Gradle project, `JacocoConfig` is NOT needed." + - " Please apply `jacoco` plugin instead." - ) - } - return projects - } - } - - private fun configure() { - configureVersion() - configureTask() - } - - private fun configureVersion() { - val jacoco = rootProject.the() - jacoco.toolVersion = Jacoco.version - } - - private fun configureTask() { - val tasks = rootProject.tasks - val copyReports = registerCopy(tasks) - val rootReport = registerRootReport(tasks, copyReports) - tasks.named(check.name) { - dependsOn(rootReport) - } - } - - private fun registerRootReport( - tasks: TaskContainer, - copyReports: TaskProvider - ): TaskProvider { - val allSourceSets = Projects(projects).sourceSets() - val mainSrcDirs = allSourceSets.mainSrcDirs() - val humanProducedSourceFolders = - FileFilter.producedByHuman(mainSrcDirs) - - val filter = CodebaseFilter( - rootProject, - mainSrcDirs, - allSourceSets.mainOutputs() - ) - 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) - sourceDirectories.from(humanProducedSourceFolders) - executionData.from(rootProject.fileTree(reportsDir)) - - classDirectories.from(humanProducedCompiledFiles) - additionalClassDirs.from(humanProducedCompiledFiles) - - reports { - html.required.set(true) - xml.required.set(true) - csv.required.set(false) - } - onlyIf { true } - } - return rootReport - } - - private fun registerCopy(tasks: TaskContainer): TaskProvider { - val everyExecData = mutableListOf() - projects.forEach { project -> - val jacocoTestReport = project.getTask(jacocoTestReport.name) - jacocoTestReport.dependsOn(project.tasks.withType(Test::class.java)) - val executionData = jacocoTestReport.executionData - everyExecData.add(executionData) - } - - 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 { - "${UUID.randomUUID()}.exec" - } - dependsOn(projects.map { it.getTask(jacocoTestReport.name) }) - } - return copyReports - } -} - -/** - * Extensions for working with groups of Gradle `Project`s. - */ -private class Projects( - private val projects: Iterable -) { - - /** - * Returns all source sets for this group of projects. - */ - fun sourceSets(): SourceSets { - val sets = projects.asSequence().map { it.sourceSets }.toList() - return SourceSets(sets) - } -} - -/** - * Extensions for working with several of Gradle `SourceSetContainer`s. - */ -private class SourceSets( - private val sourceSets: Iterable -) { - - /** - * Returns the union of Java and Kotlin source folders corresponding to the `main` - * source set across all underlying [SourceSetContainer]s. - * - * Kotlin source directories are registered as a separate [SourceDirectorySet] - * extension on the source set, not exposed via [allJava][org.gradle.api.tasks.SourceSet.getAllJava]. - * They are surfaced explicitly here so that generated Kotlin code (for example, - * the output of `protoc-gen-kotlin`) is visible to the coverage filter alongside - * the Java sources. - */ - fun mainSrcDirs(): Set { - return sourceSets - .asSequence() - .flatMap { container -> - val main = container["main"] - val javaDirs = main.allJava.srcDirs - val kotlinDirs = (main.extensions.findByName("kotlin") as? SourceDirectorySet) - ?.srcDirs - ?: emptySet() - javaDirs + kotlinDirs - } - .toSet() - } - - /** - * Returns all source set outputs corresponding to the `main` source set type. - */ - fun mainOutputs(): Set { - return sourceSets - .asSequence() - .map { it["main"].output } - .toSet() - } -} 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 deleted file mode 100644 index 9eccd6f998..0000000000 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/PathMarker.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 - -/** - * 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) { - - /** - * Generated files. - */ - GENERATED("generated"), - - /** - * Among compiler output folders, highlights those containing the compilation result - * for the `main` source set, whether produced by `javac` or `kotlinc`. - */ - MAIN_OUTPUT_FOLDER("/main/"), - - /** - * Anonymous class. - */ - ANONYMOUS_CLASS("$"); - - /** - * The number of symbols in the marker. - */ - val length: Int - get() = this.infix.length -} 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 deleted file mode 100644 index ad13dac5f2..0000000000 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/TaskName.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 - -/** - * 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, - copyReports, - - check, - jacocoTestReport -} diff --git a/buildSrc/src/main/kotlin/module.gradle.kts b/buildSrc/src/main/kotlin/module.gradle.kts index ae18b2200d..728e5971bb 100644 --- a/buildSrc/src/main/kotlin/module.gradle.kts +++ b/buildSrc/src/main/kotlin/module.gradle.kts @@ -64,7 +64,7 @@ plugins { pmd id("dokka-setup") `maven-publish` - jacoco + id("org.jetbrains.kotlinx.kover") id("project-report") id("pmd-settings") } From f150b4cf031bf683b035eb9690151460ed7dab42 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 1 Jun 2026 18:43:08 +0100 Subject: [PATCH 06/26] Update dependency reports --- docs/dependencies/dependencies.md | 127 +++++++++++++----------------- docs/dependencies/pom.xml | 44 +++++++---- 2 files changed, 83 insertions(+), 88 deletions(-) diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md index 81952ba300..b600a0311f 100644 --- a/docs/dependencies/dependencies.md +++ b/docs/dependencies/dependencies.md @@ -1,6 +1,6 @@ -# Dependencies of `io.spine.tools:validation-context:2.0.0-SNAPSHOT.445` +# Dependencies of `io.spine.tools:validation-context:2.0.0-SNAPSHOT.446` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.21.3. @@ -747,9 +747,6 @@ 1. **Group** : org.jacoco. **Name** : org.jacoco.agent. **Version** : 0.8.14. * **License:** [EPL-2.0](https://www.eclipse.org/legal/epl-2.0/) -1. **Group** : org.jacoco. **Name** : org.jacoco.ant. **Version** : 0.8.14. - * **License:** [EPL-2.0](https://www.eclipse.org/legal/epl-2.0/) - 1. **Group** : org.jacoco. **Name** : org.jacoco.core. **Version** : 0.8.14. * **License:** [EPL-2.0](https://www.eclipse.org/legal/epl-2.0/) @@ -1090,61 +1087,61 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu May 28 18:39:14 WEST 2026** using +This report was generated on **Mon Jun 01 18:40:59 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-context-tests:2.0.0-SNAPSHOT.445` +# Dependencies of `io.spine.tools:validation-context-tests:2.0.0-SNAPSHOT.446` ## Runtime -1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.20.0. +1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.21.3. * **Project URL:** [https://github.com/FasterXML/jackson-bom](https://github.com/FasterXML/jackson-bom) * **License:** [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.fasterxml.jackson.core. **Name** : jackson-annotations. **Version** : 2.20. +1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-annotations. **Version** : 2.21. * **Project URL:** [https://github.com/FasterXML/jackson](https://github.com/FasterXML/jackson) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-core. **Version** : 2.20.0. +1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-core. **Version** : 2.21.3. * **Project URL:** [https://github.com/FasterXML/jackson-core](https://github.com/FasterXML/jackson-core) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-databind. **Version** : 2.20.0. +1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-databind. **Version** : 2.21.3. * **Project URL:** [https://github.com/FasterXML/jackson](https://github.com/FasterXML/jackson) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.dataformat. **Name** : jackson-dataformat-yaml. **Version** : 2.20.0. +1. **Group** : com.fasterxml.jackson.dataformat. **Name** : jackson-dataformat-yaml. **Version** : 2.21.3. * **Project URL:** [https://github.com/FasterXML/jackson-dataformats-text](https://github.com/FasterXML/jackson-dataformats-text) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-guava. **Version** : 2.20.0. +1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-guava. **Version** : 2.21.3. * **Project URL:** [https://github.com/FasterXML/jackson-datatypes-collections](https://github.com/FasterXML/jackson-datatypes-collections) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-jdk8. **Version** : 2.20.0. +1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-jdk8. **Version** : 2.21.3. * **Project URL:** [https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8](https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8) * **License:** [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.fasterxml.jackson.datatype. **Name** : jackson-datatype-jsr310. **Version** : 2.20.0. +1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-jsr310. **Version** : 2.21.3. * **Project URL:** [https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310](https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310) * **License:** [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.fasterxml.jackson.module. **Name** : jackson-module-kotlin. **Version** : 2.20.0. +1. **Group** : com.fasterxml.jackson.module. **Name** : jackson-module-kotlin. **Version** : 2.21.3. * **Project URL:** [https://github.com/FasterXML/jackson-module-kotlin](https://github.com/FasterXML/jackson-module-kotlin) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.module. **Name** : jackson-module-parameter-names. **Version** : 2.20.0. +1. **Group** : com.fasterxml.jackson.module. **Name** : jackson-module-parameter-names. **Version** : 2.21.3. * **Project URL:** [https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names](https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names) * **License:** [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) @@ -1324,56 +1321,56 @@ This report was generated on **Thu May 28 18:39:14 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.yaml. **Name** : snakeyaml. **Version** : 2.4. +1. **Group** : org.yaml. **Name** : snakeyaml. **Version** : 2.5. * **Project URL:** [https://bitbucket.org/snakeyaml/snakeyaml](https://bitbucket.org/snakeyaml/snakeyaml) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) ## Compile, tests, and tooling -1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.20.0. +1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.21.3. * **Project URL:** [https://github.com/FasterXML/jackson-bom](https://github.com/FasterXML/jackson-bom) * **License:** [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.fasterxml.jackson.core. **Name** : jackson-annotations. **Version** : 2.20. +1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-annotations. **Version** : 2.21. * **Project URL:** [https://github.com/FasterXML/jackson](https://github.com/FasterXML/jackson) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-core. **Version** : 2.20.0. +1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-core. **Version** : 2.21.3. * **Project URL:** [https://github.com/FasterXML/jackson-core](https://github.com/FasterXML/jackson-core) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-databind. **Version** : 2.20.0. +1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-databind. **Version** : 2.21.3. * **Project URL:** [https://github.com/FasterXML/jackson](https://github.com/FasterXML/jackson) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.dataformat. **Name** : jackson-dataformat-yaml. **Version** : 2.20.0. +1. **Group** : com.fasterxml.jackson.dataformat. **Name** : jackson-dataformat-yaml. **Version** : 2.21.3. * **Project URL:** [https://github.com/FasterXML/jackson-dataformats-text](https://github.com/FasterXML/jackson-dataformats-text) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-guava. **Version** : 2.20.0. +1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-guava. **Version** : 2.21.3. * **Project URL:** [https://github.com/FasterXML/jackson-datatypes-collections](https://github.com/FasterXML/jackson-datatypes-collections) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-jdk8. **Version** : 2.20.0. +1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-jdk8. **Version** : 2.21.3. * **Project URL:** [https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8](https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8) * **License:** [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.fasterxml.jackson.datatype. **Name** : jackson-datatype-jsr310. **Version** : 2.20.0. +1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-jsr310. **Version** : 2.21.3. * **Project URL:** [https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310](https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310) * **License:** [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.fasterxml.jackson.module. **Name** : jackson-module-kotlin. **Version** : 2.20.0. +1. **Group** : com.fasterxml.jackson.module. **Name** : jackson-module-kotlin. **Version** : 2.21.3. * **Project URL:** [https://github.com/FasterXML/jackson-module-kotlin](https://github.com/FasterXML/jackson-module-kotlin) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.module. **Name** : jackson-module-parameter-names. **Version** : 2.20.0. +1. **Group** : com.fasterxml.jackson.module. **Name** : jackson-module-parameter-names. **Version** : 2.21.3. * **Project URL:** [https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names](https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names) * **License:** [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) @@ -1784,35 +1781,35 @@ This report was generated on **Thu May 28 18:39:14 WEST 2026** using * **License:** [BSD-3-Clause](https://asm.ow2.io/license.html) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.yaml. **Name** : snakeyaml. **Version** : 2.4. +1. **Group** : org.yaml. **Name** : snakeyaml. **Version** : 2.5. * **Project URL:** [https://bitbucket.org/snakeyaml/snakeyaml](https://bitbucket.org/snakeyaml/snakeyaml) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu May 28 18:39:14 WEST 2026** using +This report was generated on **Mon Jun 01 18:40:57 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-docs:2.0.0-SNAPSHOT.445` +# Dependencies of `io.spine.tools:validation-docs:2.0.0-SNAPSHOT.446` ## Runtime ## Compile, tests, and tooling The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu May 28 18:39:12 WEST 2026** using +This report was generated on **Mon Jun 01 18:40:53 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-gradle-plugin:2.0.0-SNAPSHOT.445` +# Dependencies of `io.spine.tools:validation-gradle-plugin:2.0.0-SNAPSHOT.446` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.21.3. @@ -2501,9 +2498,6 @@ This report was generated on **Thu May 28 18:39:12 WEST 2026** using 1. **Group** : org.jacoco. **Name** : org.jacoco.agent. **Version** : 0.8.14. * **License:** [EPL-2.0](https://www.eclipse.org/legal/epl-2.0/) -1. **Group** : org.jacoco. **Name** : org.jacoco.ant. **Version** : 0.8.14. - * **License:** [EPL-2.0](https://www.eclipse.org/legal/epl-2.0/) - 1. **Group** : org.jacoco. **Name** : org.jacoco.core. **Version** : 0.8.14. * **License:** [EPL-2.0](https://www.eclipse.org/legal/epl-2.0/) @@ -2864,14 +2858,14 @@ This report was generated on **Thu May 28 18:39:12 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 18:39:14 WEST 2026** using +This report was generated on **Mon Jun 01 18:40:58 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-java:2.0.0-SNAPSHOT.445` +# Dependencies of `io.spine.tools:validation-java:2.0.0-SNAPSHOT.446` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.21.3. @@ -3618,9 +3612,6 @@ This report was generated on **Thu May 28 18:39:14 WEST 2026** using 1. **Group** : org.jacoco. **Name** : org.jacoco.agent. **Version** : 0.8.14. * **License:** [EPL-2.0](https://www.eclipse.org/legal/epl-2.0/) -1. **Group** : org.jacoco. **Name** : org.jacoco.ant. **Version** : 0.8.14. - * **License:** [EPL-2.0](https://www.eclipse.org/legal/epl-2.0/) - 1. **Group** : org.jacoco. **Name** : org.jacoco.core. **Version** : 0.8.14. * **License:** [EPL-2.0](https://www.eclipse.org/legal/epl-2.0/) @@ -3961,14 +3952,14 @@ This report was generated on **Thu May 28 18:39:14 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 18:39:14 WEST 2026** using +This report was generated on **Mon Jun 01 18:40:59 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-java-bundle:2.0.0-SNAPSHOT.445` +# Dependencies of `io.spine.tools:validation-java-bundle:2.0.0-SNAPSHOT.446` ## Runtime 1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 13.0. @@ -4015,14 +4006,14 @@ This report was generated on **Thu May 28 18:39:14 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 18:39:12 WEST 2026** using +This report was generated on **Mon Jun 01 18:40:54 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-java-settings:2.0.0-SNAPSHOT.445` +# Dependencies of `io.spine.tools:validation-java-settings:2.0.0-SNAPSHOT.446` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -4459,9 +4450,6 @@ This report was generated on **Thu May 28 18:39:12 WEST 2026** using 1. **Group** : org.jacoco. **Name** : org.jacoco.agent. **Version** : 0.8.14. * **License:** [EPL-2.0](https://www.eclipse.org/legal/epl-2.0/) -1. **Group** : org.jacoco. **Name** : org.jacoco.ant. **Version** : 0.8.14. - * **License:** [EPL-2.0](https://www.eclipse.org/legal/epl-2.0/) - 1. **Group** : org.jacoco. **Name** : org.jacoco.core. **Version** : 0.8.14. * **License:** [EPL-2.0](https://www.eclipse.org/legal/epl-2.0/) @@ -4798,14 +4786,14 @@ This report was generated on **Thu May 28 18:39:12 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 18:39:14 WEST 2026** using +This report was generated on **Mon Jun 01 18:40:58 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-validation-jvm-runtime:2.0.0-SNAPSHOT.445` +# Dependencies of `io.spine:spine-validation-jvm-runtime:2.0.0-SNAPSHOT.446` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -5266,9 +5254,6 @@ This report was generated on **Thu May 28 18:39:14 WEST 2026** using 1. **Group** : org.jacoco. **Name** : org.jacoco.agent. **Version** : 0.8.14. * **License:** [EPL-2.0](https://www.eclipse.org/legal/epl-2.0/) -1. **Group** : org.jacoco. **Name** : org.jacoco.ant. **Version** : 0.8.14. - * **License:** [EPL-2.0](https://www.eclipse.org/legal/epl-2.0/) - 1. **Group** : org.jacoco. **Name** : org.jacoco.core. **Version** : 0.8.14. * **License:** [EPL-2.0](https://www.eclipse.org/legal/epl-2.0/) @@ -5605,14 +5590,14 @@ This report was generated on **Thu May 28 18:39:14 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 18:39:14 WEST 2026** using +This report was generated on **Mon Jun 01 18:40:58 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-consumer:2.0.0-SNAPSHOT.445` +# Dependencies of `io.spine.tools:validation-consumer:2.0.0-SNAPSHOT.446` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.21.3. @@ -6294,14 +6279,14 @@ This report was generated on **Thu May 28 18:39:14 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 18:39:13 WEST 2026** using +This report was generated on **Mon Jun 01 18:40:56 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-consumer-dependency:2.0.0-SNAPSHOT.445` +# Dependencies of `io.spine.tools:validation-consumer-dependency:2.0.0-SNAPSHOT.446` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -6759,14 +6744,14 @@ This report was generated on **Thu May 28 18:39:13 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 18:39:14 WEST 2026** using +This report was generated on **Mon Jun 01 18:40:57 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-extensions:2.0.0-SNAPSHOT.445` +# Dependencies of `io.spine.tools:validation-extensions:2.0.0-SNAPSHOT.446` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.21.3. @@ -7385,14 +7370,14 @@ This report was generated on **Thu May 28 18:39:14 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 18:39:19 WEST 2026** using +This report was generated on **Mon Jun 01 18:41:00 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-runtime:2.0.0-SNAPSHOT.445` +# Dependencies of `io.spine.tools:validation-runtime:2.0.0-SNAPSHOT.446` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -7953,14 +7938,14 @@ This report was generated on **Thu May 28 18:39:19 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 18:39:23 WEST 2026** using +This report was generated on **Mon Jun 01 18:41:04 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-time:2.0.0-SNAPSHOT.445` +# Dependencies of `io.spine.tools:validation-time:2.0.0-SNAPSHOT.446` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -8382,14 +8367,14 @@ This report was generated on **Thu May 28 18:39:23 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 18:39:13 WEST 2026** using +This report was generated on **Mon Jun 01 18:40:56 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-validating:2.0.0-SNAPSHOT.445` +# Dependencies of `io.spine.tools:validation-validating:2.0.0-SNAPSHOT.446` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -8993,14 +8978,14 @@ This report was generated on **Thu May 28 18:39:13 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 18:39:26 WEST 2026** using +This report was generated on **Mon Jun 01 18:41:07 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-validator:2.0.0-SNAPSHOT.445` +# Dependencies of `io.spine.tools:validation-validator:2.0.0-SNAPSHOT.446` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.21.3. @@ -9738,14 +9723,14 @@ This report was generated on **Thu May 28 18:39:26 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 18:39:20 WEST 2026** using +This report was generated on **Mon Jun 01 18:41:00 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-validator-dependency:2.0.0-SNAPSHOT.445` +# Dependencies of `io.spine.tools:validation-validator-dependency:2.0.0-SNAPSHOT.446` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -9978,14 +9963,14 @@ This report was generated on **Thu May 28 18:39:20 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 18:39:13 WEST 2026** using +This report was generated on **Mon Jun 01 18:40:55 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-vanilla:2.0.0-SNAPSHOT.445` +# Dependencies of `io.spine.tools:validation-vanilla:2.0.0-SNAPSHOT.446` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -10328,6 +10313,6 @@ This report was generated on **Thu May 28 18:39:13 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 18:39:13 WEST 2026** using +This report was generated on **Mon Jun 01 18:40:56 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file diff --git a/docs/dependencies/pom.xml b/docs/dependencies/pom.xml index 9c563bda8d..e18223fa88 100644 --- a/docs/dependencies/pom.xml +++ b/docs/dependencies/pom.xml @@ -10,7 +10,7 @@ all modules and does not describe the project structure per-subproject. --> io.spine.tools validation -2.0.0-SNAPSHOT.445 +2.0.0-SNAPSHOT.446 2015 @@ -50,13 +50,13 @@ all modules and does not describe the project structure per-subproject. io.spine spine-base - 2.0.0-SNAPSHOT.390 + 2.0.0-SNAPSHOT.391 compile io.spine spine-format - 2.0.0-SNAPSHOT.390 + 2.0.0-SNAPSHOT.391 compile @@ -74,43 +74,43 @@ all modules and does not describe the project structure per-subproject. io.spine spine-validation-jvm-runtime - 2.0.0-SNAPSHOT.444 + 2.0.0-SNAPSHOT.445 compile io.spine.tools compiler-backend - 2.0.0-SNAPSHOT.044 + 2.0.0-SNAPSHOT.046 compile io.spine.tools compiler-gradle-api - 2.0.0-SNAPSHOT.044 + 2.0.0-SNAPSHOT.046 compile io.spine.tools compiler-gradle-plugin - 2.0.0-SNAPSHOT.044 + 2.0.0-SNAPSHOT.046 compile io.spine.tools compiler-jvm - 2.0.0-SNAPSHOT.044 + 2.0.0-SNAPSHOT.046 compile io.spine.tools compiler-params - 2.0.0-SNAPSHOT.044 + 2.0.0-SNAPSHOT.046 compile io.spine.tools jvm-tools - 2.0.0-SNAPSHOT.378 + 2.0.0-SNAPSHOT.381 compile @@ -182,13 +182,13 @@ all modules and does not describe the project structure per-subproject. io.spine.tools compiler-api - 2.0.0-SNAPSHOT.044 + 2.0.0-SNAPSHOT.046 test io.spine.tools compiler-testlib - 2.0.0-SNAPSHOT.044 + 2.0.0-SNAPSHOT.046 test @@ -277,17 +277,17 @@ all modules and does not describe the project structure per-subproject. io.spine.tools compiler-cli-all - 2.0.0-SNAPSHOT.044 + 2.0.0-SNAPSHOT.046 io.spine.tools compiler-protoc-plugin - 2.0.0-SNAPSHOT.044 + 2.0.0-SNAPSHOT.046 io.spine.tools core-jvm-plugins - 2.0.0-SNAPSHOT.067 + 2.0.0-SNAPSHOT.068 io.spine.tools @@ -307,7 +307,7 @@ all modules and does not describe the project structure per-subproject. io.spine.tools validation-java-bundle - 2.0.0-SNAPSHOT.444 + 2.0.0-SNAPSHOT.445 net.sourceforge.pmd @@ -326,7 +326,7 @@ all modules and does not describe the project structure per-subproject. org.jacoco - org.jacoco.ant + org.jacoco.report 0.8.14 @@ -359,6 +359,11 @@ all modules and does not describe the project structure per-subproject. templating-plugin 2.2.0 + + org.jetbrains.kotlin + abi-tools + 2.3.20 + org.jetbrains.kotlin kotlin-build-tools-compat @@ -369,6 +374,11 @@ all modules and does not describe the project structure per-subproject. kotlin-build-tools-impl 2.3.20 + + org.jetbrains.kotlin + kotlin-klib-commonizer-embeddable + 2.3.20 + org.jetbrains.kotlin kotlin-scripting-compiler-embeddable From 4a578838df383dd3b6ec6d8efbae711bb17c6368 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 1 Jun 2026 18:50:24 +0100 Subject: [PATCH 07/26] Add tests --- .../validation/TemplateStringExtsSpec.kt | 78 ++++++++++++++++++ .../validation/ValidatableMessageSpec.kt | 80 +++++++++++++++++++ .../spine/validation/ValidatorRegistrySpec.kt | 46 +++++++++++ .../validation/diags/ViolationTextSpec.kt | 20 +++++ 4 files changed, 224 insertions(+) create mode 100644 jvm-runtime/src/test/kotlin/io/spine/validation/TemplateStringExtsSpec.kt create mode 100644 jvm-runtime/src/test/kotlin/io/spine/validation/ValidatableMessageSpec.kt diff --git a/jvm-runtime/src/test/kotlin/io/spine/validation/TemplateStringExtsSpec.kt b/jvm-runtime/src/test/kotlin/io/spine/validation/TemplateStringExtsSpec.kt new file mode 100644 index 0000000000..a456babcf4 --- /dev/null +++ b/jvm-runtime/src/test/kotlin/io/spine/validation/TemplateStringExtsSpec.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.validation + +import com.google.protobuf.Timestamp +import io.kotest.matchers.maps.shouldContain +import io.spine.code.proto.FieldDeclaration +import io.spine.validation.StandardPlaceholder.FIELD_PATH +import io.spine.validation.StandardPlaceholder.FIELD_TYPE +import io.spine.validation.StandardPlaceholder.GOES_COMPANION +import io.spine.validation.StandardPlaceholder.PARENT_TYPE +import io.spine.validation.StandardPlaceholder.REGEX_PATTERN +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`TemplateStringExts` should") +internal class TemplateStringExtsSpec { + + private val secondsField = FieldDeclaration( + Timestamp.getDescriptor().findFieldByNumber(Timestamp.SECONDS_FIELD_NUMBER) + ) + + @Test + fun `fill placeholders from a field declaration`() { + val template = io.spine.string.TemplateString.newBuilder() + .withField(secondsField) + .build() + + template.placeholderValueMap.let { + it shouldContain (FIELD_PATH.value.name to "seconds") + it shouldContain (FIELD_TYPE.value.name to "long") + it shouldContain (PARENT_TYPE.value.name to "google.protobuf.Timestamp") + } + } + + @Test + fun `fill companion placeholder`() { + val template = io.spine.string.TemplateString.newBuilder() + .withCompanion(secondsField) + .build() + + template.placeholderValueMap shouldContain (GOES_COMPANION.value.name to "seconds") + } + + @Test + fun `fill regex pattern placeholder`() { + val pattern = "^[a-z]+$" + val template = io.spine.string.TemplateString.newBuilder() + .withRegex(pattern) + .build() + + template.placeholderValueMap shouldContain (REGEX_PATTERN.value.name to pattern) + } +} diff --git a/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatableMessageSpec.kt b/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatableMessageSpec.kt new file mode 100644 index 0000000000..856dfc5c59 --- /dev/null +++ b/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatableMessageSpec.kt @@ -0,0 +1,80 @@ +/* + * 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.validation + +import com.google.protobuf.Descriptors +import com.google.protobuf.Empty +import com.google.protobuf.Message +import com.google.protobuf.Parser +import com.google.protobuf.UnknownFieldSet +import io.kotest.matchers.optional.shouldBeEmpty +import io.kotest.matchers.shouldBe +import io.spine.base.FieldPath +import io.spine.type.TypeName +import java.util.Optional +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`ValidatableMessage` should") +internal class ValidatableMessageSpec { + + @Test + fun `provide a default 'validate' method`() { + val message = StubValidatableMessage() + message.validate().shouldBeEmpty() + + message.capturedPath shouldBe FieldPath.getDefaultInstance() + message.capturedName shouldBe null + } +} + +/** + * A stub implementation of [ValidatableMessage] for testing the default [validate] method. + */ +private class StubValidatableMessage : ValidatableMessage, com.google.protobuf.AbstractMessage() { + + var capturedPath: FieldPath? = null + var capturedName: TypeName? = null + + override fun validate(parentPath: FieldPath, parentName: TypeName?): Optional { + capturedPath = parentPath + capturedName = parentName + return Optional.empty() + } + + override fun getDefaultInstanceForType(): Message = this + override fun getDescriptorForType(): Descriptors.Descriptor = Empty.getDescriptor() + override fun getAllFields(): Map = emptyMap() + override fun hasField(field: Descriptors.FieldDescriptor?): Boolean = false + override fun getField(field: Descriptors.FieldDescriptor?): Any = Any() + override fun getRepeatedFieldCount(field: Descriptors.FieldDescriptor?): Int = 0 + override fun getRepeatedField(field: Descriptors.FieldDescriptor?, index: Int): Any = Any() + override fun getUnknownFields(): UnknownFieldSet = UnknownFieldSet.getDefaultInstance() + override fun getParserForType(): Parser = throw UnsupportedOperationException() + override fun newBuilderForType(): Message.Builder = throw UnsupportedOperationException() + override fun toBuilder(): Message.Builder = throw UnsupportedOperationException() +} diff --git a/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatorRegistrySpec.kt b/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatorRegistrySpec.kt index a30319ba2a..4683f18050 100644 --- a/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatorRegistrySpec.kt +++ b/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatorRegistrySpec.kt @@ -26,13 +26,16 @@ package io.spine.validation +import com.google.protobuf.Int64Value import com.google.protobuf.Timestamp import com.google.protobuf.timestamp import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.shouldBe +import io.spine.base.FieldPath import io.spine.string.templateString +import io.spine.type.TypeName import java.util.ServiceLoader import java.util.concurrent.Executors import java.util.concurrent.TimeUnit @@ -204,6 +207,49 @@ internal class ValidatorRegistrySpec { executor.awaitTermination(1, TimeUnit.MINUTES) } } + + @Test + fun `report violations with parent path and name`() { + val validator = AlwaysInvalidTimestampValidator() + ValidatorRegistry.add(Timestamp::class, validator) + + val timestamp = timestamp { seconds = 100 } + val parentPath = FieldPath.newBuilder().addFieldName("some_parent_field").build() + val parentName = TypeName.of("spine.test.ParentMessage") + + val violations = ValidatorRegistry.validate(timestamp, parentPath, parentName) + + violations shouldHaveSize 1 + val violation = violations[0] + violation.typeName shouldBe parentName.value + violation.fieldPath shouldBe parentPath + } + + @Test + fun `report violations with custom field path and value`() { + val fieldPath = FieldPath.newBuilder().addFieldName("seconds").build() + val fieldValue = Int64Value.of(100) + val validator = object : MessageValidator { + override fun validate(message: Timestamp): List { + return listOf(FieldViolation( + templateString { withPlaceholders = "Invalid field" }, + fieldPath, + fieldValue + )) + } + } + ValidatorRegistry.add(Timestamp::class, validator) + + val timestamp = timestamp { seconds = 100 } + val parentPath = FieldPath.newBuilder().addFieldName("parent").build() + + val violations = ValidatorRegistry.validate(timestamp, parentPath, null) + + violations shouldHaveSize 1 + val violation = violations[0] + violation.fieldPath.fieldNameList shouldContainExactly listOf("parent", "seconds") + violation.fieldValue.unpack(Int64Value::class.java) shouldBe fieldValue + } } private class AlwaysInvalidTimestampValidator : MessageValidator { diff --git a/jvm-runtime/src/test/kotlin/io/spine/validation/diags/ViolationTextSpec.kt b/jvm-runtime/src/test/kotlin/io/spine/validation/diags/ViolationTextSpec.kt index 852c12078a..4538c222a7 100644 --- a/jvm-runtime/src/test/kotlin/io/spine/validation/diags/ViolationTextSpec.kt +++ b/jvm-runtime/src/test/kotlin/io/spine/validation/diags/ViolationTextSpec.kt @@ -28,8 +28,11 @@ package io.spine.validation.diags import com.google.common.testing.NullPointerTester import com.google.protobuf.Timestamp +import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain import io.spine.base.Field +import io.spine.option.IfMissingOption +import io.spine.option.OptionsProto import io.spine.type.TypeName import io.spine.validation.ViolationText import io.spine.validation.constraintViolation @@ -70,4 +73,21 @@ internal class ViolationTextSpec { text shouldContain ViolationText.of(first).toString() text shouldContain ViolationText.of(second).toString() } + + @Test + fun `return custom error message if present`() { + val customMsg = "Custom error message" + val option = IfMissingOption.getDefaultInstance() + ViolationText.errorMessage(option, customMsg) shouldBe customMsg + } + + @Test + fun `return default error message if custom is empty`() { + val option = IfMissingOption.getDefaultInstance() + val defaultMsg = option.descriptorForType + .options + .getExtension(OptionsProto.defaultMessage) + + ViolationText.errorMessage(option, "") shouldBe defaultMsg + } } From 4cc1678f192a167ffab96d2beef8ede19c388651 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 1 Jun 2026 18:53:25 +0100 Subject: [PATCH 08/26] Update dependency reports --- docs/dependencies/dependencies.md | 34 +++++++++++++++---------------- docs/dependencies/pom.xml | 10 --------- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md index b600a0311f..abfa1b48c4 100644 --- a/docs/dependencies/dependencies.md +++ b/docs/dependencies/dependencies.md @@ -1087,7 +1087,7 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:40:59 WEST 2026** using +This report was generated on **Mon Jun 01 18:52:12 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). @@ -1788,7 +1788,7 @@ This report was generated on **Mon Jun 01 18:40:59 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:40:57 WEST 2026** using +This report was generated on **Mon Jun 01 18:52:10 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). @@ -1802,7 +1802,7 @@ This report was generated on **Mon Jun 01 18:40:57 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:40:53 WEST 2026** using +This report was generated on **Mon Jun 01 18:52:06 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). @@ -2858,7 +2858,7 @@ This report was generated on **Mon Jun 01 18:40:53 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:40:58 WEST 2026** using +This report was generated on **Mon Jun 01 18:52:12 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). @@ -3952,7 +3952,7 @@ This report was generated on **Mon Jun 01 18:40:58 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:40:59 WEST 2026** using +This report was generated on **Mon Jun 01 18:52:12 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). @@ -4006,7 +4006,7 @@ This report was generated on **Mon Jun 01 18:40:59 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:40:54 WEST 2026** using +This report was generated on **Mon Jun 01 18:52:06 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). @@ -4786,7 +4786,7 @@ This report was generated on **Mon Jun 01 18:40:54 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:40:58 WEST 2026** using +This report was generated on **Mon Jun 01 18:52:12 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). @@ -5590,7 +5590,7 @@ This report was generated on **Mon Jun 01 18:40:58 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:40:58 WEST 2026** using +This report was generated on **Mon Jun 01 18:52:12 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). @@ -6279,7 +6279,7 @@ This report was generated on **Mon Jun 01 18:40:58 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:40:56 WEST 2026** using +This report was generated on **Mon Jun 01 18:52:09 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). @@ -6744,7 +6744,7 @@ This report was generated on **Mon Jun 01 18:40:56 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:40:57 WEST 2026** using +This report was generated on **Mon Jun 01 18:52:11 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). @@ -7370,7 +7370,7 @@ This report was generated on **Mon Jun 01 18:40:57 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:41:00 WEST 2026** using +This report was generated on **Mon Jun 01 18:52:12 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). @@ -7938,7 +7938,7 @@ This report was generated on **Mon Jun 01 18:41:00 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:41:04 WEST 2026** using +This report was generated on **Mon Jun 01 18:52:20 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). @@ -8367,7 +8367,7 @@ This report was generated on **Mon Jun 01 18:41:04 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:40:56 WEST 2026** using +This report was generated on **Mon Jun 01 18:52:10 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). @@ -8978,7 +8978,7 @@ This report was generated on **Mon Jun 01 18:40:56 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:41:07 WEST 2026** using +This report was generated on **Mon Jun 01 18:52:25 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). @@ -9723,7 +9723,7 @@ This report was generated on **Mon Jun 01 18:41:07 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:41:00 WEST 2026** using +This report was generated on **Mon Jun 01 18:52:12 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). @@ -9963,7 +9963,7 @@ This report was generated on **Mon Jun 01 18:41:00 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:40:55 WEST 2026** using +This report was generated on **Mon Jun 01 18:52:08 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). @@ -10313,6 +10313,6 @@ This report was generated on **Mon Jun 01 18:40:55 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:40:56 WEST 2026** using +This report was generated on **Mon Jun 01 18:52:10 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 e18223fa88..e375c3c1fb 100644 --- a/docs/dependencies/pom.xml +++ b/docs/dependencies/pom.xml @@ -359,11 +359,6 @@ all modules and does not describe the project structure per-subproject. templating-plugin 2.2.0 - - org.jetbrains.kotlin - abi-tools - 2.3.20 - org.jetbrains.kotlin kotlin-build-tools-compat @@ -374,11 +369,6 @@ all modules and does not describe the project structure per-subproject. kotlin-build-tools-impl 2.3.20 - - org.jetbrains.kotlin - kotlin-klib-commonizer-embeddable - 2.3.20 - org.jetbrains.kotlin kotlin-scripting-compiler-embeddable From b0797744215c4168ebfd5f997adb76ef1186689f Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 1 Jun 2026 18:59:00 +0100 Subject: [PATCH 09/26] Bump Validation in examples --- docs/content/docs/validation/developer/build-and-release.md | 2 +- .../validation/user/01-getting-started/adding-to-build.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/content/docs/validation/developer/build-and-release.md b/docs/content/docs/validation/developer/build-and-release.md index 5a88d6c69d..da3cc71af5 100644 --- a/docs/content/docs/validation/developer/build-and-release.md +++ b/docs/content/docs/validation/developer/build-and-release.md @@ -47,7 +47,7 @@ through Gradle's `extra` properties: end="val validationVersion"> ```kotlin -val validationVersion by extra("2.0.0-SNAPSHOT.444") +val validationVersion by extra("2.0.0-SNAPSHOT.446") ``` The root build script applies this file under `allprojects { … }` and assigns diff --git a/docs/content/docs/validation/user/01-getting-started/adding-to-build.md b/docs/content/docs/validation/user/01-getting-started/adding-to-build.md index a16a543dc8..05a792cfdc 100644 --- a/docs/content/docs/validation/user/01-getting-started/adding-to-build.md +++ b/docs/content/docs/validation/user/01-getting-started/adding-to-build.md @@ -90,7 +90,7 @@ Add the Validation plugin to the build. ```kotlin plugins { module - id("io.spine.validation") version "2.0.0-SNAPSHOT.444" + id("io.spine.validation") version "2.0.0-SNAPSHOT.446" } ``` @@ -120,7 +120,7 @@ adding Validation directly. CoreJvm brings in the Validation Gradle plugin for y ```kotlin plugins { module - id("io.spine.core-jvm") version "2.0.0-SNAPSHOT.065" + id("io.spine.core-jvm") version "2.0.0-SNAPSHOT.068" } ``` From 775917e5261003d2be6f73570d50b60fa60b9423 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 1 Jun 2026 18:59:20 +0100 Subject: [PATCH 10/26] Update `_examples` ref. --- docs/_examples | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_examples b/docs/_examples index c6f6433d35..e536e1a95b 160000 --- a/docs/_examples +++ b/docs/_examples @@ -1 +1 @@ -Subproject commit c6f6433d35d5f6e50d3b4d06fb949b4de792258c +Subproject commit e536e1a95bc00b9e780ea5c91d131f153a38e596 From 00dbf8410c8b51e61c2d85b8b1f74266abad8f13 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Mon, 1 Jun 2026 21:01:56 +0300 Subject: [PATCH 11/26] Fix code layout Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../test/kotlin/io/spine/validation/TemplateStringExtsSpec.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jvm-runtime/src/test/kotlin/io/spine/validation/TemplateStringExtsSpec.kt b/jvm-runtime/src/test/kotlin/io/spine/validation/TemplateStringExtsSpec.kt index a456babcf4..badf63c952 100644 --- a/jvm-runtime/src/test/kotlin/io/spine/validation/TemplateStringExtsSpec.kt +++ b/jvm-runtime/src/test/kotlin/io/spine/validation/TemplateStringExtsSpec.kt @@ -49,7 +49,7 @@ internal class TemplateStringExtsSpec { val template = io.spine.string.TemplateString.newBuilder() .withField(secondsField) .build() - + template.placeholderValueMap.let { it shouldContain (FIELD_PATH.value.name to "seconds") it shouldContain (FIELD_TYPE.value.name to "long") From f9d1dd974dca48e88ce534f7a64a2eaab59bdde2 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Mon, 1 Jun 2026 21:06:14 +0300 Subject: [PATCH 12/26] Fix code layout Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../test/kotlin/io/spine/validation/TemplateStringExtsSpec.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jvm-runtime/src/test/kotlin/io/spine/validation/TemplateStringExtsSpec.kt b/jvm-runtime/src/test/kotlin/io/spine/validation/TemplateStringExtsSpec.kt index badf63c952..7815be26f7 100644 --- a/jvm-runtime/src/test/kotlin/io/spine/validation/TemplateStringExtsSpec.kt +++ b/jvm-runtime/src/test/kotlin/io/spine/validation/TemplateStringExtsSpec.kt @@ -62,7 +62,7 @@ internal class TemplateStringExtsSpec { val template = io.spine.string.TemplateString.newBuilder() .withCompanion(secondsField) .build() - + template.placeholderValueMap shouldContain (GOES_COMPANION.value.name to "seconds") } From 775603386f29e6a94fe38f38e171e024d29d0e94 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Mon, 1 Jun 2026 21:06:32 +0300 Subject: [PATCH 13/26] Fix code layout Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../test/kotlin/io/spine/validation/TemplateStringExtsSpec.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jvm-runtime/src/test/kotlin/io/spine/validation/TemplateStringExtsSpec.kt b/jvm-runtime/src/test/kotlin/io/spine/validation/TemplateStringExtsSpec.kt index 7815be26f7..72f8da09d6 100644 --- a/jvm-runtime/src/test/kotlin/io/spine/validation/TemplateStringExtsSpec.kt +++ b/jvm-runtime/src/test/kotlin/io/spine/validation/TemplateStringExtsSpec.kt @@ -72,7 +72,7 @@ internal class TemplateStringExtsSpec { val template = io.spine.string.TemplateString.newBuilder() .withRegex(pattern) .build() - + template.placeholderValueMap shouldContain (REGEX_PATTERN.value.name to pattern) } } From 9104a305e6872a97d66d2abd7506c0ad12f0d96c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 18:05:41 +0000 Subject: [PATCH 14/26] Address PR review feedback on tests and root plugins --- build.gradle.kts | 1 - .../kotlin/io/spine/validation/ValidatorRegistrySpec.kt | 9 +++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 6456f7006c..532d750051 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -73,7 +73,6 @@ buildscript { } plugins { - base idea `gradle-doctor` id("project-report") diff --git a/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatorRegistrySpec.kt b/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatorRegistrySpec.kt index 4683f18050..7bb526021e 100644 --- a/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatorRegistrySpec.kt +++ b/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatorRegistrySpec.kt @@ -39,6 +39,7 @@ import io.spine.type.TypeName import java.util.ServiceLoader import java.util.concurrent.Executors import java.util.concurrent.TimeUnit +import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -47,6 +48,14 @@ import org.junit.jupiter.api.assertDoesNotThrow @DisplayName("`ValidatorRegistry` should") internal class ValidatorRegistrySpec { + companion object { + @JvmStatic + @BeforeAll + fun cleanRegistry() { + ValidatorRegistry.clear() + } + } + @BeforeEach fun setUp() { ValidatorRegistry.clear() From 054899c2b480040ce7392a9d02cf437b115634a4 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Mon, 1 Jun 2026 21:08:08 +0300 Subject: [PATCH 15/26] Fix code layout Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> From 0edfacb6bc0da051823e53d0e81ef1a98fbbab78 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 1 Jun 2026 19:10:25 +0100 Subject: [PATCH 16/26] Update dependency reports --- docs/dependencies/dependencies.md | 32 +++++++++++++++---------------- docs/dependencies/pom.xml | 10 ++++++++++ 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md index abfa1b48c4..d6a296d2cd 100644 --- a/docs/dependencies/dependencies.md +++ b/docs/dependencies/dependencies.md @@ -1087,7 +1087,7 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:52:12 WEST 2026** using +This report was generated on **Mon Jun 01 19:06:02 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). @@ -1788,7 +1788,7 @@ This report was generated on **Mon Jun 01 18:52:12 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:52:10 WEST 2026** using +This report was generated on **Mon Jun 01 19:06:01 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). @@ -2858,7 +2858,7 @@ This report was generated on **Mon Jun 01 18:52:06 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:52:12 WEST 2026** using +This report was generated on **Mon Jun 01 19:06:02 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). @@ -3952,7 +3952,7 @@ This report was generated on **Mon Jun 01 18:52:12 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:52:12 WEST 2026** using +This report was generated on **Mon Jun 01 19:06:02 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). @@ -4006,7 +4006,7 @@ This report was generated on **Mon Jun 01 18:52:12 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:52:06 WEST 2026** using +This report was generated on **Mon Jun 01 19:06:00 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). @@ -4786,7 +4786,7 @@ This report was generated on **Mon Jun 01 18:52:06 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:52:12 WEST 2026** using +This report was generated on **Mon Jun 01 19:06:01 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). @@ -5590,7 +5590,7 @@ This report was generated on **Mon Jun 01 18:52:12 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:52:12 WEST 2026** using +This report was generated on **Mon Jun 01 19:06:02 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). @@ -6279,7 +6279,7 @@ This report was generated on **Mon Jun 01 18:52:12 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:52:09 WEST 2026** using +This report was generated on **Mon Jun 01 19:06:01 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). @@ -6744,7 +6744,7 @@ This report was generated on **Mon Jun 01 18:52:09 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:52:11 WEST 2026** using +This report was generated on **Mon Jun 01 19:06:02 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). @@ -7370,7 +7370,7 @@ This report was generated on **Mon Jun 01 18:52:11 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:52:12 WEST 2026** using +This report was generated on **Mon Jun 01 19:06:06 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). @@ -7938,7 +7938,7 @@ This report was generated on **Mon Jun 01 18:52:12 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:52:20 WEST 2026** using +This report was generated on **Mon Jun 01 19:06:11 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). @@ -8367,7 +8367,7 @@ This report was generated on **Mon Jun 01 18:52:20 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:52:10 WEST 2026** using +This report was generated on **Mon Jun 01 19:06:01 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). @@ -8978,7 +8978,7 @@ This report was generated on **Mon Jun 01 18:52:10 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:52:25 WEST 2026** using +This report was generated on **Mon Jun 01 19:06:13 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). @@ -9723,7 +9723,7 @@ This report was generated on **Mon Jun 01 18:52:25 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:52:12 WEST 2026** using +This report was generated on **Mon Jun 01 19:06:07 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). @@ -9963,7 +9963,7 @@ This report was generated on **Mon Jun 01 18:52:12 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:52:08 WEST 2026** using +This report was generated on **Mon Jun 01 19:06:00 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). @@ -10313,6 +10313,6 @@ This report was generated on **Mon Jun 01 18:52:08 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 18:52:10 WEST 2026** using +This report was generated on **Mon Jun 01 19:06:01 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 e375c3c1fb..e18223fa88 100644 --- a/docs/dependencies/pom.xml +++ b/docs/dependencies/pom.xml @@ -359,6 +359,11 @@ all modules and does not describe the project structure per-subproject. templating-plugin 2.2.0 + + org.jetbrains.kotlin + abi-tools + 2.3.20 + org.jetbrains.kotlin kotlin-build-tools-compat @@ -369,6 +374,11 @@ all modules and does not describe the project structure per-subproject. kotlin-build-tools-impl 2.3.20 + + org.jetbrains.kotlin + kotlin-klib-commonizer-embeddable + 2.3.20 + org.jetbrains.kotlin kotlin-scripting-compiler-embeddable From ee8abd8bcc528d0d16d7a212d7e7dff478d73268 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 1 Jun 2026 19:18:45 +0100 Subject: [PATCH 17/26] Fix inheritace order --- .../test/kotlin/io/spine/validation/ValidatableMessageSpec.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatableMessageSpec.kt b/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatableMessageSpec.kt index 856dfc5c59..06edb9d637 100644 --- a/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatableMessageSpec.kt +++ b/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatableMessageSpec.kt @@ -26,6 +26,7 @@ package io.spine.validation +import com.google.protobuf.AbstractMessage import com.google.protobuf.Descriptors import com.google.protobuf.Empty import com.google.protobuf.Message @@ -55,7 +56,7 @@ internal class ValidatableMessageSpec { /** * A stub implementation of [ValidatableMessage] for testing the default [validate] method. */ -private class StubValidatableMessage : ValidatableMessage, com.google.protobuf.AbstractMessage() { +private class StubValidatableMessage : AbstractMessage(), ValidatableMessage { var capturedPath: FieldPath? = null var capturedName: TypeName? = null From ccae81ac9c187bd0bb480168d58c8ebdc4235c31 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 1 Jun 2026 19:19:14 +0100 Subject: [PATCH 18/26] Add `base` plugin to the root build --- build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle.kts b/build.gradle.kts index 532d750051..6456f7006c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -73,6 +73,7 @@ buildscript { } plugins { + base idea `gradle-doctor` id("project-report") From 41c8f906c8ef4658f6b3ed69aa81c2768a3592ff Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 1 Jun 2026 19:24:31 +0100 Subject: [PATCH 19/26] Remove duplicated clean-up --- .../kotlin/io/spine/validation/ValidatorRegistrySpec.kt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatorRegistrySpec.kt b/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatorRegistrySpec.kt index 7bb526021e..0fd6b06e3a 100644 --- a/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatorRegistrySpec.kt +++ b/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatorRegistrySpec.kt @@ -48,14 +48,6 @@ import org.junit.jupiter.api.assertDoesNotThrow @DisplayName("`ValidatorRegistry` should") internal class ValidatorRegistrySpec { - companion object { - @JvmStatic - @BeforeAll - fun cleanRegistry() { - ValidatorRegistry.clear() - } - } - @BeforeEach fun setUp() { ValidatorRegistry.clear() From bd4725ad751c08910e015c92f24b00c7ec87d499 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 1 Jun 2026 19:29:34 +0100 Subject: [PATCH 20/26] Optimise imports --- .../test/kotlin/io/spine/validation/ValidatorRegistrySpec.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatorRegistrySpec.kt b/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatorRegistrySpec.kt index 0fd6b06e3a..b4076cb362 100644 --- a/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatorRegistrySpec.kt +++ b/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatorRegistrySpec.kt @@ -36,10 +36,9 @@ import io.kotest.matchers.shouldBe import io.spine.base.FieldPath import io.spine.string.templateString import io.spine.type.TypeName -import java.util.ServiceLoader +import java.util.* import java.util.concurrent.Executors import java.util.concurrent.TimeUnit -import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test From 77048c11a5d721d7d295448aed8db4406aec27ce Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 1 Jun 2026 21:12:23 +0100 Subject: [PATCH 21/26] Cover `jvm-runtime` with more tests --- .../spine/validation/DetectedViolationSpec.kt | 52 ++++++ .../spine/validation/FieldAwareMessageSpec.kt | 120 ++++++++++++++ .../spine/validation/MessageExtensionsSpec.kt | 149 ++++++++++++++++++ .../validation/StandardPlaceholderSpec.kt | 36 +++++ .../validation/TimestampValidatorSpec.kt | 15 ++ .../io/spine/validation/ValidateSpec.kt | 142 +++++++++++++++++ .../validation/ValidationExceptionSpec.kt | 9 ++ 7 files changed, 523 insertions(+) create mode 100644 jvm-runtime/src/test/kotlin/io/spine/validation/DetectedViolationSpec.kt create mode 100644 jvm-runtime/src/test/kotlin/io/spine/validation/FieldAwareMessageSpec.kt create mode 100644 jvm-runtime/src/test/kotlin/io/spine/validation/MessageExtensionsSpec.kt create mode 100644 jvm-runtime/src/test/kotlin/io/spine/validation/StandardPlaceholderSpec.kt create mode 100644 jvm-runtime/src/test/kotlin/io/spine/validation/ValidateSpec.kt diff --git a/jvm-runtime/src/test/kotlin/io/spine/validation/DetectedViolationSpec.kt b/jvm-runtime/src/test/kotlin/io/spine/validation/DetectedViolationSpec.kt new file mode 100644 index 0000000000..058d1ac7f6 --- /dev/null +++ b/jvm-runtime/src/test/kotlin/io/spine/validation/DetectedViolationSpec.kt @@ -0,0 +1,52 @@ +/* + * 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.validation + +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.shouldBe +import io.spine.base.fieldPath +import io.spine.string.templateString +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("detected violations should") +internal class DetectedViolationSpec { + + @Test + fun `allow field violations without field value`() { + val message = templateString { withPlaceholders = "Invalid field" } + val path = fieldPath { + fieldName.add("field") + } + + val violation = FieldViolation(message, path) + + violation.message shouldBe message + violation.fieldPath shouldBe path + violation.fieldValue.shouldBeNull() + } +} diff --git a/jvm-runtime/src/test/kotlin/io/spine/validation/FieldAwareMessageSpec.kt b/jvm-runtime/src/test/kotlin/io/spine/validation/FieldAwareMessageSpec.kt new file mode 100644 index 0000000000..c4a81bc098 --- /dev/null +++ b/jvm-runtime/src/test/kotlin/io/spine/validation/FieldAwareMessageSpec.kt @@ -0,0 +1,120 @@ +/* + * 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.validation + +import com.google.protobuf.AbstractMessage +import com.google.protobuf.Descriptors +import com.google.protobuf.Descriptors.Descriptor +import com.google.protobuf.Descriptors.FieldDescriptor +import com.google.protobuf.Message +import com.google.protobuf.Parser +import com.google.protobuf.StringValue +import com.google.protobuf.UnknownFieldSet +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +@DisplayName("`FieldAwareMessage` should") +internal class FieldAwareMessageSpec { + + private val field = StringValue.getDescriptor().findFieldByName("value") + + @Test + fun `read field values through the default protobuf API`() { + val message = DefaultFieldAwareMessage("stored") + + message.readValue(field) shouldBe "stored" + } + + @Test + fun `confirm that all field values are reachable`() { + val message = StubFieldAwareMessage("stored") + + message.checkFieldsReachable() shouldBe true + } + + @Test + fun `reject mismatching field-aware reads`() { + val message = StubFieldAwareMessage("stored", readValue = "different") + + val thrown = assertThrows { + message.checkFieldsReachable() + } + + thrown.message shouldContain "`readValue(field)` is implemented incorrectly" + } +} + +private class DefaultFieldAwareMessage( + private val fieldValue: String +) : AbstractMessage(), FieldAwareMessage { + + override fun getDefaultInstanceForType(): Message = this + override fun getDescriptorForType(): Descriptor = StringValue.getDescriptor() + override fun getAllFields(): Map = + mapOf(valueField to fieldValue) + override fun hasField(field: FieldDescriptor?): Boolean = field == valueField + override fun getField(field: FieldDescriptor?): Any = fieldValue + override fun getRepeatedFieldCount(field: FieldDescriptor?): Int = 0 + override fun getRepeatedField(field: FieldDescriptor?, index: Int): Any = Any() + override fun getUnknownFields(): UnknownFieldSet = UnknownFieldSet.getDefaultInstance() + override fun getParserForType(): Parser = throw UnsupportedOperationException() + override fun newBuilderForType(): Message.Builder = throw UnsupportedOperationException() + override fun toBuilder(): Message.Builder = throw UnsupportedOperationException() + + private companion object { + val valueField: FieldDescriptor = + StringValue.getDescriptor().findFieldByName("value") + } +} + +private class StubFieldAwareMessage( + private val fieldValue: String, + private val readValue: String = fieldValue +) : AbstractMessage(), FieldAwareMessage { + + override fun readValue(field: FieldDescriptor): Any = readValue + override fun getDefaultInstanceForType(): Message = this + override fun getDescriptorForType(): Descriptor = StringValue.getDescriptor() + override fun getAllFields(): Map = + mapOf(valueField to fieldValue) + override fun hasField(field: FieldDescriptor?): Boolean = field == valueField + override fun getField(field: FieldDescriptor?): Any = fieldValue + override fun getRepeatedFieldCount(field: FieldDescriptor?): Int = 0 + override fun getRepeatedField(field: FieldDescriptor?, index: Int): Any = Any() + override fun getUnknownFields(): UnknownFieldSet = UnknownFieldSet.getDefaultInstance() + override fun getParserForType(): Parser = throw UnsupportedOperationException() + override fun newBuilderForType(): Message.Builder = throw UnsupportedOperationException() + override fun toBuilder(): Message.Builder = throw UnsupportedOperationException() + + private companion object { + val valueField: FieldDescriptor = + StringValue.getDescriptor().findFieldByName("value") + } +} diff --git a/jvm-runtime/src/test/kotlin/io/spine/validation/MessageExtensionsSpec.kt b/jvm-runtime/src/test/kotlin/io/spine/validation/MessageExtensionsSpec.kt new file mode 100644 index 0000000000..440d6d9adc --- /dev/null +++ b/jvm-runtime/src/test/kotlin/io/spine/validation/MessageExtensionsSpec.kt @@ -0,0 +1,149 @@ +/* + * 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.validation + +import com.google.protobuf.AbstractMessage +import com.google.protobuf.Descriptors +import com.google.protobuf.Descriptors.Descriptor +import com.google.protobuf.Descriptors.FieldDescriptor +import com.google.protobuf.Message +import com.google.protobuf.Parser +import com.google.protobuf.StringValue +import com.google.protobuf.UnknownFieldSet +import io.kotest.matchers.shouldBe +import io.spine.base.FieldPath +import io.spine.type.TypeName +import java.util.Optional +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("message validation extensions should") +internal class MessageExtensionsSpec { + + @Test + fun `delegate 'checkValid' to 'Validate_check'`() { + val message = CopyableValidatableMessage("valid") + + message.checkValid() shouldBe message + } + + @Test + fun `copy validatable messages through their validating builder`() { + val message = CopyableValidatableMessage("initial") + + val copied = message.copy { + value = "changed" + } + + copied.value shouldBe "changed" + } + + @Test + @Suppress("DEPRECATION") + fun `delegate deprecated 'vBuild' to 'build'`() { + val builder = CopyableValidatingBuilder("built") + + builder.vBuild().value shouldBe "built" + } +} + +private class CopyableValidatableMessage( + val value: String +) : AbstractMessage(), ValidatableMessage { + + override fun validate(parentPath: FieldPath, parentName: TypeName?): Optional = + Optional.empty() + + override fun getDefaultInstanceForType(): Message = CopyableValidatableMessage("") + override fun getDescriptorForType(): Descriptor = StringValue.getDescriptor() + override fun getAllFields(): Map = mapOf(valueField to value) + override fun hasField(field: FieldDescriptor?): Boolean = field == valueField + override fun getField(field: FieldDescriptor?): Any = value + override fun getRepeatedFieldCount(field: FieldDescriptor?): Int = 0 + override fun getRepeatedField(field: FieldDescriptor?, index: Int): Any = Any() + override fun getUnknownFields(): UnknownFieldSet = UnknownFieldSet.getDefaultInstance() + override fun getParserForType(): Parser = throw UnsupportedOperationException() + override fun newBuilderForType(): Message.Builder = CopyableValidatingBuilder() + override fun toBuilder(): Message.Builder = CopyableValidatingBuilder(value) + + companion object { + val valueField: FieldDescriptor = + StringValue.getDescriptor().findFieldByName("value") + } +} + +private class CopyableValidatingBuilder( + var value: String = "" +) : AbstractMessage.Builder(), + ValidatingBuilder { + + override fun build(): CopyableValidatableMessage = CopyableValidatableMessage(value) + override fun buildPartial(): CopyableValidatableMessage = CopyableValidatableMessage(value) + override fun clone(): CopyableValidatingBuilder = CopyableValidatingBuilder(value) + override fun isInitialized(): Boolean = true + override fun getDescriptorForType(): Descriptor = StringValue.getDescriptor() + + override fun newBuilderForField(field: FieldDescriptor): Message.Builder = + CopyableValidatingBuilder() + + override fun setField( + field: FieldDescriptor, + value: Any + ): CopyableValidatingBuilder { + this.value = value as String + return this + } + + override fun clearField(field: FieldDescriptor): CopyableValidatingBuilder { + value = "" + return this + } + + override fun setRepeatedField( + field: FieldDescriptor, + index: Int, + value: Any + ): CopyableValidatingBuilder = this + + override fun addRepeatedField( + field: FieldDescriptor, + value: Any + ): CopyableValidatingBuilder = this + + override fun setUnknownFields(unknownFields: UnknownFieldSet): CopyableValidatingBuilder = this + override fun getDefaultInstanceForType(): Message = CopyableValidatableMessage("") + override fun getAllFields(): Map = + mapOf(CopyableValidatableMessage.valueField to value) + + override fun hasField(field: FieldDescriptor?): Boolean = + field == CopyableValidatableMessage.valueField + + override fun getField(field: FieldDescriptor?): Any = value + override fun getRepeatedFieldCount(field: FieldDescriptor?): Int = 0 + override fun getRepeatedField(field: FieldDescriptor?, index: Int): Any = Any() + override fun getUnknownFields(): UnknownFieldSet = UnknownFieldSet.getDefaultInstance() +} diff --git a/jvm-runtime/src/test/kotlin/io/spine/validation/StandardPlaceholderSpec.kt b/jvm-runtime/src/test/kotlin/io/spine/validation/StandardPlaceholderSpec.kt new file mode 100644 index 0000000000..95df12fa74 --- /dev/null +++ b/jvm-runtime/src/test/kotlin/io/spine/validation/StandardPlaceholderSpec.kt @@ -0,0 +1,36 @@ +/* + * 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, 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.validation + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`StandardPlaceholder` should") +internal class StandardPlaceholderSpec { + + @Test + fun `expose placeholder text`() { + StandardPlaceholder.FIELD_PATH.placed shouldBe "\${field.path}" + } +} diff --git a/jvm-runtime/src/test/kotlin/io/spine/validation/TimestampValidatorSpec.kt b/jvm-runtime/src/test/kotlin/io/spine/validation/TimestampValidatorSpec.kt index c5e3b9d353..9d4e88f5bc 100644 --- a/jvm-runtime/src/test/kotlin/io/spine/validation/TimestampValidatorSpec.kt +++ b/jvm-runtime/src/test/kotlin/io/spine/validation/TimestampValidatorSpec.kt @@ -66,6 +66,21 @@ internal class TimestampValidatorSpec { violation.fieldValue shouldBe secondsValue } + @Test + fun `detect seconds above the maximum timestamp`() { + val secondsValue = 253402300800L // One second after the maximum. + val timestamp = timestamp { + seconds = secondsValue + } + + val violations = validator.validate(timestamp) + + violations shouldHaveSize 1 + val violation = violations[0] as FieldViolation + violation.fieldPath!!.fieldNameList shouldBe listOf("seconds") + violation.fieldValue shouldBe secondsValue + } + @Test fun `detect an invalid timestamp with nanos out of range`() { val nanosValue = 1_000_000_000 diff --git a/jvm-runtime/src/test/kotlin/io/spine/validation/ValidateSpec.kt b/jvm-runtime/src/test/kotlin/io/spine/validation/ValidateSpec.kt new file mode 100644 index 0000000000..ef2249ad1f --- /dev/null +++ b/jvm-runtime/src/test/kotlin/io/spine/validation/ValidateSpec.kt @@ -0,0 +1,142 @@ +/* + * 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.validation + +import com.google.protobuf.AbstractMessage +import com.google.protobuf.Descriptors +import com.google.protobuf.Descriptors.Descriptor +import com.google.protobuf.Descriptors.FieldDescriptor +import com.google.protobuf.Empty +import com.google.protobuf.Message +import com.google.protobuf.Parser +import com.google.protobuf.Any as ProtoAny +import com.google.protobuf.Timestamp +import com.google.protobuf.UnknownFieldSet +import com.google.protobuf.timestamp +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.spine.base.FieldPath +import io.spine.protobuf.AnyPacker +import io.spine.string.templateString +import io.spine.type.TypeName +import java.util.Optional +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +@DisplayName("`Validate` should") +internal class ValidateSpec { + + @BeforeEach + fun setUp() { + ValidatorRegistry.clear() + } + + @Test + fun `return a valid message passed to 'check'`() { + val message = Timestamp.getDefaultInstance() + + Validate.check(message) shouldBe message + } + + @Test + fun `throw if a message passed to 'check' is invalid`() { + val violation = constraintViolation { + message = templateString { withPlaceholders = "Invalid" } + } + val message = StubValidateMessage(listOf(violation)) + + val thrown = assertThrows { + Validate.check(message) + } + + thrown.constraintViolations shouldBe listOf(violation) + } + + @Test + fun `unpack known 'Any' messages before validation`() { + ValidatorRegistry.add(Timestamp::class, TimestampValidator()) + val timestamp = timestamp { + nanos = -1 + } + val packed = AnyPacker.pack(timestamp) + + val violations = Validate.violationsOf(packed) + + violations shouldHaveSize 1 + violations.single().fieldPath.fieldNameList shouldBe listOf("nanos") + } + + @Test + fun `skip unpacking unknown 'Any' messages`() { + val packed = ProtoAny.newBuilder() + .setTypeUrl("type.example/unknown.Message") + .build() + + Validate.violationsOf(packed).shouldBeEmpty() + } + + @Test + fun `use violations returned by a 'ValidatableMessage'`() { + val violation = constraintViolation { + message = templateString { withPlaceholders = "From message" } + } + val message = StubValidateMessage(listOf(violation)) + + Validate.violationsOf(message) shouldBe listOf(violation) + } +} + +private class StubValidateMessage( + private val violations: List +) : AbstractMessage(), ValidatableMessage { + + override fun validate(parentPath: FieldPath, parentName: TypeName?): Optional { + return if (violations.isEmpty()) { + Optional.empty() + } else { + Optional.of(validationError { + constraintViolation.addAll(violations) + }) + } + } + + override fun getDefaultInstanceForType(): Message = this + override fun getDescriptorForType(): Descriptor = Empty.getDescriptor() + override fun getAllFields(): Map = emptyMap() + override fun hasField(field: FieldDescriptor?): Boolean = false + override fun getField(field: FieldDescriptor?): kotlin.Any = kotlin.Any() + override fun getRepeatedFieldCount(field: FieldDescriptor?): Int = 0 + override fun getRepeatedField(field: FieldDescriptor?, index: Int): kotlin.Any = + kotlin.Any() + override fun getUnknownFields(): UnknownFieldSet = UnknownFieldSet.getDefaultInstance() + override fun getParserForType(): Parser = throw UnsupportedOperationException() + override fun newBuilderForType(): Message.Builder = throw UnsupportedOperationException() + override fun toBuilder(): Message.Builder = throw UnsupportedOperationException() +} diff --git a/jvm-runtime/src/test/kotlin/io/spine/validation/ValidationExceptionSpec.kt b/jvm-runtime/src/test/kotlin/io/spine/validation/ValidationExceptionSpec.kt index 09a6e03e63..4d843aed60 100644 --- a/jvm-runtime/src/test/kotlin/io/spine/validation/ValidationExceptionSpec.kt +++ b/jvm-runtime/src/test/kotlin/io/spine/validation/ValidationExceptionSpec.kt @@ -58,4 +58,13 @@ internal class ValidationExceptionSpec { error.details.unpackKnownType() shouldBe exception.asMessage() } + + @Test + @Suppress("DEPRECATION") + fun `provide deprecated validation error view`() { + val violation = ConstraintViolation.newBuilder().build() + val exception = ValidationException(violation) + + exception.asValidationError() shouldBe exception.asMessage() + } } From 9518fc6a1af46c0db148f5292a32425b0263a915 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 1 Jun 2026 21:12:32 +0100 Subject: [PATCH 22/26] Update dependency reports --- docs/dependencies/dependencies.md | 32 +++++++++++++++---------------- docs/dependencies/pom.xml | 10 ---------- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md index d6a296d2cd..e2fce48a00 100644 --- a/docs/dependencies/dependencies.md +++ b/docs/dependencies/dependencies.md @@ -1087,7 +1087,7 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 19:06:02 WEST 2026** using +This report was generated on **Mon Jun 01 21:11:25 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). @@ -1788,7 +1788,7 @@ This report was generated on **Mon Jun 01 19:06:02 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 19:06:01 WEST 2026** using +This report was generated on **Mon Jun 01 21:11:24 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). @@ -2858,7 +2858,7 @@ This report was generated on **Mon Jun 01 18:52:06 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 19:06:02 WEST 2026** using +This report was generated on **Mon Jun 01 21:11:24 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). @@ -3952,7 +3952,7 @@ This report was generated on **Mon Jun 01 19:06:02 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 19:06:02 WEST 2026** using +This report was generated on **Mon Jun 01 21:11:25 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). @@ -4006,7 +4006,7 @@ This report was generated on **Mon Jun 01 19:06:02 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 19:06:00 WEST 2026** using +This report was generated on **Mon Jun 01 21:11:19 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). @@ -4786,7 +4786,7 @@ This report was generated on **Mon Jun 01 19:06:00 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 19:06:01 WEST 2026** using +This report was generated on **Mon Jun 01 21:11:25 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). @@ -5590,7 +5590,7 @@ This report was generated on **Mon Jun 01 19:06:01 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 19:06:02 WEST 2026** using +This report was generated on **Mon Jun 01 21:11:24 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). @@ -6279,7 +6279,7 @@ This report was generated on **Mon Jun 01 19:06:02 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 19:06:01 WEST 2026** using +This report was generated on **Mon Jun 01 21:11:23 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). @@ -6744,7 +6744,7 @@ This report was generated on **Mon Jun 01 19:06:01 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 19:06:02 WEST 2026** using +This report was generated on **Mon Jun 01 21:11:24 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). @@ -7370,7 +7370,7 @@ This report was generated on **Mon Jun 01 19:06:02 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 19:06:06 WEST 2026** using +This report was generated on **Mon Jun 01 21:11:27 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). @@ -7938,7 +7938,7 @@ This report was generated on **Mon Jun 01 19:06:06 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 19:06:11 WEST 2026** using +This report was generated on **Mon Jun 01 21:11:33 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). @@ -8367,7 +8367,7 @@ This report was generated on **Mon Jun 01 19:06:11 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 19:06:01 WEST 2026** using +This report was generated on **Mon Jun 01 21:11:23 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). @@ -8978,7 +8978,7 @@ This report was generated on **Mon Jun 01 19:06:01 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 19:06:13 WEST 2026** using +This report was generated on **Mon Jun 01 21:11:38 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). @@ -9723,7 +9723,7 @@ This report was generated on **Mon Jun 01 19:06:13 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 19:06:07 WEST 2026** using +This report was generated on **Mon Jun 01 21:11:27 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). @@ -9963,7 +9963,7 @@ This report was generated on **Mon Jun 01 19:06:07 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 19:06:00 WEST 2026** using +This report was generated on **Mon Jun 01 21:11:21 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). @@ -10313,6 +10313,6 @@ This report was generated on **Mon Jun 01 19:06:00 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 19:06:01 WEST 2026** using +This report was generated on **Mon Jun 01 21:11:23 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 e18223fa88..e375c3c1fb 100644 --- a/docs/dependencies/pom.xml +++ b/docs/dependencies/pom.xml @@ -359,11 +359,6 @@ all modules and does not describe the project structure per-subproject. templating-plugin 2.2.0 - - org.jetbrains.kotlin - abi-tools - 2.3.20 - org.jetbrains.kotlin kotlin-build-tools-compat @@ -374,11 +369,6 @@ all modules and does not describe the project structure per-subproject. kotlin-build-tools-impl 2.3.20 - - org.jetbrains.kotlin - kotlin-klib-commonizer-embeddable - 2.3.20 - org.jetbrains.kotlin kotlin-scripting-compiler-embeddable From 314fb2035e630580c141d07f6761b2b15122d4f1 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 1 Jun 2026 21:19:13 +0100 Subject: [PATCH 23/26] Optimise imports --- .../test/kotlin/io/spine/validation/FieldAwareMessageSpec.kt | 1 - .../test/kotlin/io/spine/validation/MessageExtensionsSpec.kt | 3 +-- .../kotlin/io/spine/validation/ValidatableMessageSpec.kt | 2 +- .../src/test/kotlin/io/spine/validation/ValidateSpec.kt | 5 ++--- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/jvm-runtime/src/test/kotlin/io/spine/validation/FieldAwareMessageSpec.kt b/jvm-runtime/src/test/kotlin/io/spine/validation/FieldAwareMessageSpec.kt index c4a81bc098..6a1d0044d1 100644 --- a/jvm-runtime/src/test/kotlin/io/spine/validation/FieldAwareMessageSpec.kt +++ b/jvm-runtime/src/test/kotlin/io/spine/validation/FieldAwareMessageSpec.kt @@ -27,7 +27,6 @@ package io.spine.validation import com.google.protobuf.AbstractMessage -import com.google.protobuf.Descriptors import com.google.protobuf.Descriptors.Descriptor import com.google.protobuf.Descriptors.FieldDescriptor import com.google.protobuf.Message diff --git a/jvm-runtime/src/test/kotlin/io/spine/validation/MessageExtensionsSpec.kt b/jvm-runtime/src/test/kotlin/io/spine/validation/MessageExtensionsSpec.kt index 440d6d9adc..60d3655228 100644 --- a/jvm-runtime/src/test/kotlin/io/spine/validation/MessageExtensionsSpec.kt +++ b/jvm-runtime/src/test/kotlin/io/spine/validation/MessageExtensionsSpec.kt @@ -27,7 +27,6 @@ package io.spine.validation import com.google.protobuf.AbstractMessage -import com.google.protobuf.Descriptors import com.google.protobuf.Descriptors.Descriptor import com.google.protobuf.Descriptors.FieldDescriptor import com.google.protobuf.Message @@ -37,7 +36,7 @@ import com.google.protobuf.UnknownFieldSet import io.kotest.matchers.shouldBe import io.spine.base.FieldPath import io.spine.type.TypeName -import java.util.Optional +import java.util.* import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test diff --git a/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatableMessageSpec.kt b/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatableMessageSpec.kt index 06edb9d637..e1f20a4bce 100644 --- a/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatableMessageSpec.kt +++ b/jvm-runtime/src/test/kotlin/io/spine/validation/ValidatableMessageSpec.kt @@ -36,7 +36,7 @@ import io.kotest.matchers.optional.shouldBeEmpty import io.kotest.matchers.shouldBe import io.spine.base.FieldPath import io.spine.type.TypeName -import java.util.Optional +import java.util.* import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test diff --git a/jvm-runtime/src/test/kotlin/io/spine/validation/ValidateSpec.kt b/jvm-runtime/src/test/kotlin/io/spine/validation/ValidateSpec.kt index ef2249ad1f..aa7a65d3f5 100644 --- a/jvm-runtime/src/test/kotlin/io/spine/validation/ValidateSpec.kt +++ b/jvm-runtime/src/test/kotlin/io/spine/validation/ValidateSpec.kt @@ -27,13 +27,11 @@ package io.spine.validation import com.google.protobuf.AbstractMessage -import com.google.protobuf.Descriptors import com.google.protobuf.Descriptors.Descriptor import com.google.protobuf.Descriptors.FieldDescriptor import com.google.protobuf.Empty import com.google.protobuf.Message import com.google.protobuf.Parser -import com.google.protobuf.Any as ProtoAny import com.google.protobuf.Timestamp import com.google.protobuf.UnknownFieldSet import com.google.protobuf.timestamp @@ -44,11 +42,12 @@ import io.spine.base.FieldPath import io.spine.protobuf.AnyPacker import io.spine.string.templateString import io.spine.type.TypeName -import java.util.Optional +import java.util.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import com.google.protobuf.Any as ProtoAny @DisplayName("`Validate` should") internal class ValidateSpec { From 65d33596714c13f8a755c9e9ae95f727d8cff4cd Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 1 Jun 2026 21:21:01 +0100 Subject: [PATCH 24/26] Update dependency reports --- docs/dependencies/dependencies.md | 32 +++++++++++++++---------------- docs/dependencies/pom.xml | 10 ++++++++++ 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md index e2fce48a00..b8bd7a7e03 100644 --- a/docs/dependencies/dependencies.md +++ b/docs/dependencies/dependencies.md @@ -1087,7 +1087,7 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:11:25 WEST 2026** using +This report was generated on **Mon Jun 01 21:19:53 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). @@ -1788,7 +1788,7 @@ This report was generated on **Mon Jun 01 21:11:25 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:11:24 WEST 2026** using +This report was generated on **Mon Jun 01 21:19:53 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). @@ -2858,7 +2858,7 @@ This report was generated on **Mon Jun 01 18:52:06 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:11:24 WEST 2026** using +This report was generated on **Mon Jun 01 21:19:53 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). @@ -3952,7 +3952,7 @@ This report was generated on **Mon Jun 01 21:11:24 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:11:25 WEST 2026** using +This report was generated on **Mon Jun 01 21:19:53 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). @@ -4006,7 +4006,7 @@ This report was generated on **Mon Jun 01 21:11:25 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:11:19 WEST 2026** using +This report was generated on **Mon Jun 01 21:19:52 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). @@ -4786,7 +4786,7 @@ This report was generated on **Mon Jun 01 21:11:19 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:11:25 WEST 2026** using +This report was generated on **Mon Jun 01 21:19:53 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). @@ -5590,7 +5590,7 @@ This report was generated on **Mon Jun 01 21:11:25 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:11:24 WEST 2026** using +This report was generated on **Mon Jun 01 21:19:53 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). @@ -6279,7 +6279,7 @@ This report was generated on **Mon Jun 01 21:11:24 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:11:23 WEST 2026** using +This report was generated on **Mon Jun 01 21:19:53 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). @@ -6744,7 +6744,7 @@ This report was generated on **Mon Jun 01 21:11:23 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:11:24 WEST 2026** using +This report was generated on **Mon Jun 01 21:19:55 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -7370,7 +7370,7 @@ This report was generated on **Mon Jun 01 21:11:24 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:11:27 WEST 2026** using +This report was generated on **Mon Jun 01 21:19:59 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). @@ -7938,7 +7938,7 @@ This report was generated on **Mon Jun 01 21:11:27 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:11:33 WEST 2026** using +This report was generated on **Mon Jun 01 21:20:03 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). @@ -8367,7 +8367,7 @@ This report was generated on **Mon Jun 01 21:11:33 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:11:23 WEST 2026** using +This report was generated on **Mon Jun 01 21:19:55 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -8978,7 +8978,7 @@ This report was generated on **Mon Jun 01 21:11:23 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:11:38 WEST 2026** using +This report was generated on **Mon Jun 01 21:20:07 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). @@ -9723,7 +9723,7 @@ This report was generated on **Mon Jun 01 21:11:38 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:11:27 WEST 2026** using +This report was generated on **Mon Jun 01 21:19:59 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). @@ -9963,7 +9963,7 @@ This report was generated on **Mon Jun 01 21:11:27 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:11:21 WEST 2026** using +This report was generated on **Mon Jun 01 21:19:52 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). @@ -10313,6 +10313,6 @@ This report was generated on **Mon Jun 01 21:11:21 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:11:23 WEST 2026** using +This report was generated on **Mon Jun 01 21:19:55 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file diff --git a/docs/dependencies/pom.xml b/docs/dependencies/pom.xml index e375c3c1fb..e18223fa88 100644 --- a/docs/dependencies/pom.xml +++ b/docs/dependencies/pom.xml @@ -359,6 +359,11 @@ all modules and does not describe the project structure per-subproject. templating-plugin 2.2.0 + + org.jetbrains.kotlin + abi-tools + 2.3.20 + org.jetbrains.kotlin kotlin-build-tools-compat @@ -369,6 +374,11 @@ all modules and does not describe the project structure per-subproject. kotlin-build-tools-impl 2.3.20 + + org.jetbrains.kotlin + kotlin-klib-commonizer-embeddable + 2.3.20 + org.jetbrains.kotlin kotlin-scripting-compiler-embeddable From 85e05b61b57af1018cdff4240683a0fe8b7d9b59 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 2 Jun 2026 15:54:06 +0100 Subject: [PATCH 25/26] Improve test names and display names --- .../kotlin/io/spine/validation/MessageExtensionsSpec.kt | 2 +- ...essageExtensionsSpec.kt => MessageExtensionsITest.kt} | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) rename tests/runtime/src/test/kotlin/io/spine/validation/{MessageExtensionsSpec.kt => MessageExtensionsITest.kt} (88%) diff --git a/jvm-runtime/src/test/kotlin/io/spine/validation/MessageExtensionsSpec.kt b/jvm-runtime/src/test/kotlin/io/spine/validation/MessageExtensionsSpec.kt index 60d3655228..c9816f59fa 100644 --- a/jvm-runtime/src/test/kotlin/io/spine/validation/MessageExtensionsSpec.kt +++ b/jvm-runtime/src/test/kotlin/io/spine/validation/MessageExtensionsSpec.kt @@ -40,7 +40,7 @@ import java.util.* import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -@DisplayName("message validation extensions should") +@DisplayName("Validation extensions for `Message` should") internal class MessageExtensionsSpec { @Test diff --git a/tests/runtime/src/test/kotlin/io/spine/validation/MessageExtensionsSpec.kt b/tests/runtime/src/test/kotlin/io/spine/validation/MessageExtensionsITest.kt similarity index 88% rename from tests/runtime/src/test/kotlin/io/spine/validation/MessageExtensionsSpec.kt rename to tests/runtime/src/test/kotlin/io/spine/validation/MessageExtensionsITest.kt index 618fbdbe9e..4022d64fbb 100644 --- a/tests/runtime/src/test/kotlin/io/spine/validation/MessageExtensionsSpec.kt +++ b/tests/runtime/src/test/kotlin/io/spine/validation/MessageExtensionsITest.kt @@ -35,8 +35,15 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +/** + * This is a small set of integration tests for extension functions + * working on compiled Protobuf messages. + * + * There is a "sister" test suite under `jvm-runtime` module, + * which tests the same extensions but on stubs. + */ @DisplayName("Validation extensions for `Message` should") -internal class MessageExtensionsSpec { +internal class MessageExtensionsITest { @Nested inner class `Check if the message is valid` { From dd8c421d28fd688640a26fbf0a53583e51dc2127 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 2 Jun 2026 15:57:30 +0100 Subject: [PATCH 26/26] Update dependency reports --- docs/dependencies/dependencies.md | 32 +++++++++++++++---------------- docs/dependencies/pom.xml | 10 ---------- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md index b8bd7a7e03..7d4715c589 100644 --- a/docs/dependencies/dependencies.md +++ b/docs/dependencies/dependencies.md @@ -1087,7 +1087,7 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:19:53 WEST 2026** using +This report was generated on **Tue Jun 02 15:56:59 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). @@ -1788,7 +1788,7 @@ This report was generated on **Mon Jun 01 21:19:53 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:19:53 WEST 2026** using +This report was generated on **Tue Jun 02 15:57:00 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). @@ -2858,7 +2858,7 @@ This report was generated on **Mon Jun 01 18:52:06 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:19:53 WEST 2026** using +This report was generated on **Tue Jun 02 15:56:59 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). @@ -3952,7 +3952,7 @@ This report was generated on **Mon Jun 01 21:19:53 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:19:53 WEST 2026** using +This report was generated on **Tue Jun 02 15:57:00 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). @@ -4006,7 +4006,7 @@ This report was generated on **Mon Jun 01 21:19:53 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:19:52 WEST 2026** using +This report was generated on **Tue Jun 02 15:56:54 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -4786,7 +4786,7 @@ This report was generated on **Mon Jun 01 21:19:52 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:19:53 WEST 2026** using +This report was generated on **Tue Jun 02 15:56:59 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). @@ -5590,7 +5590,7 @@ This report was generated on **Mon Jun 01 21:19:53 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:19:53 WEST 2026** using +This report was generated on **Tue Jun 02 15:56:59 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). @@ -6279,7 +6279,7 @@ This report was generated on **Mon Jun 01 21:19:53 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:19:53 WEST 2026** using +This report was generated on **Tue Jun 02 15:56:58 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). @@ -6744,7 +6744,7 @@ This report was generated on **Mon Jun 01 21:19:53 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:19:55 WEST 2026** using +This report was generated on **Tue Jun 02 15:56:58 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). @@ -7370,7 +7370,7 @@ This report was generated on **Mon Jun 01 21:19:55 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:19:59 WEST 2026** using +This report was generated on **Tue Jun 02 15:57:02 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). @@ -7938,7 +7938,7 @@ This report was generated on **Mon Jun 01 21:19:59 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:20:03 WEST 2026** using +This report was generated on **Tue Jun 02 15:57:09 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). @@ -8367,7 +8367,7 @@ This report was generated on **Mon Jun 01 21:20:03 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:19:55 WEST 2026** using +This report was generated on **Tue Jun 02 15:56:57 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). @@ -8978,7 +8978,7 @@ This report was generated on **Mon Jun 01 21:19:55 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:20:07 WEST 2026** using +This report was generated on **Tue Jun 02 15:57:15 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). @@ -9723,7 +9723,7 @@ This report was generated on **Mon Jun 01 21:20:07 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:19:59 WEST 2026** using +This report was generated on **Tue Jun 02 15:57:02 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). @@ -9963,7 +9963,7 @@ This report was generated on **Mon Jun 01 21:19:59 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:19:52 WEST 2026** using +This report was generated on **Tue Jun 02 15:56:56 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -10313,6 +10313,6 @@ This report was generated on **Mon Jun 01 21:19:52 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 21:19:55 WEST 2026** using +This report was generated on **Tue Jun 02 15:56:57 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 e18223fa88..e375c3c1fb 100644 --- a/docs/dependencies/pom.xml +++ b/docs/dependencies/pom.xml @@ -359,11 +359,6 @@ all modules and does not describe the project structure per-subproject. templating-plugin 2.2.0 - - org.jetbrains.kotlin - abi-tools - 2.3.20 - org.jetbrains.kotlin kotlin-build-tools-compat @@ -374,11 +369,6 @@ all modules and does not describe the project structure per-subproject. kotlin-build-tools-impl 2.3.20 - - org.jetbrains.kotlin - kotlin-klib-commonizer-embeddable - 2.3.20 - org.jetbrains.kotlin kotlin-scripting-compiler-embeddable