From 247b58a53869478a2ae694d97d86bb20fd63766b Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Mon, 1 Jun 2026 10:58:33 +0100 Subject: [PATCH 01/45] 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/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-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 + .claude/settings.local.json | 10 + AGENTS.md | 11 + buildSrc/build.gradle.kts | 4 +- buildSrc/src/main/kotlin/DokkaExts.kt | 5 +- .../src/main/kotlin/config-tester.gradle.kts | 5 +- .../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 | 330 +++++++++++++ .../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 +- .../src/main/kotlin/write-manifest.gradle.kts | 6 +- config | 2 +- 80 files changed, 3849 insertions(+), 165 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-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 .claude/settings.local.json 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/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-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/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000000..602e6ceaaa --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Skill(pre-pr)", + "Skill(pre-pr:*)", + "Bash(.agents/skills/version-bumped/scripts/version-bumped.sh)", + "Bash(echo \"exit=$?\")" + ] + } +} diff --git a/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/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..bc1a6d3c64 --- /dev/null +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/KoverConfig.kt @@ -0,0 +1,330 @@ +/* + * 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.gradle.kotlin.dsl.configure +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 + * [io.spine.gradle.report.coverage.JacocoConfig]. The behaviour mirrors what + * `JacocoConfig.applyTo(rootProject)` 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/write-manifest.gradle.kts b/buildSrc/src/main/kotlin/write-manifest.gradle.kts index b63d3272da..49130c0c4b 100644 --- a/buildSrc/src/main/kotlin/write-manifest.gradle.kts +++ b/buildSrc/src/main/kotlin/write-manifest.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +import io.spine.gradle.SpineTaskGroup import io.spine.gradle.publish.SpinePublishing import java.nio.file.Files.createDirectories import java.nio.file.Files.createFile @@ -105,6 +106,9 @@ val manifestAttributes = mapOf( */ val exposeManifestForTests by tasks.registering { + group = SpineTaskGroup.name + description = "Writes a `MANIFEST.MF` to `resources/main` so that it is visible to tests" + val outputFile = layout.buildDirectory.file("resources/main/META-INF/MANIFEST.MF") outputs.file(outputFile).withPropertyName("manifestFile") diff --git a/config b/config index bf604a9683..c875ea22e1 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit bf604a96834ebf062335a24fcec346e259dd649c +Subproject commit c875ea22e14836bbb71b1ac3252f08355c3a1076 From c2e285ba6c8051dec656402d5169ca2f63b9f837 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Mon, 1 Jun 2026 10:59:22 +0100 Subject: [PATCH 02/45] Bump version -> `2.0.0-SNAPSHOT.392` --- version.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle.kts b/version.gradle.kts index 63e84e8f4a..cce8164f08 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -24,4 +24,4 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -val versionToPublish: String by extra("2.0.0-SNAPSHOT.391") +val versionToPublish: String by extra("2.0.0-SNAPSHOT.392") From 473cd44eea8575964d21ff9aabe8f7b17fa7c7f7 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Mon, 1 Jun 2026 11:00:11 +0100 Subject: [PATCH 03/45] Update dependency reports --- dependencies.md | 548 ++++++++++++++++++------------ docs/dependencies/dependencies.md | 16 +- docs/dependencies/pom.xml | 2 +- 3 files changed, 349 insertions(+), 217 deletions(-) diff --git a/dependencies.md b/dependencies.md index e6a0426694..0f13835e2d 100644 --- a/dependencies.md +++ b/dependencies.md @@ -1,17 +1,17 @@ -# Dependencies of `io.spine:spine-annotations:2.0.0-SNAPSHOT.387` +# Dependencies of `io.spine:spine-annotations:2.0.0-SNAPSHOT.384` ## Runtime 1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.0.2. * **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -58,6 +58,10 @@ * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : com.github.ben-manes.caffeine. **Name** : caffeine. **Version** : 2.9.3. + * **Project URL:** [https://github.com/ben-manes/caffeine](https://github.com/ben-manes/caffeine) + * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : com.github.ben-manes.caffeine. **Name** : caffeine. **Version** : 3.0.5. * **Project URL:** [https://github.com/ben-manes/caffeine](https://github.com/ben-manes/caffeine) * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -143,15 +147,15 @@ * **Project URL:** [https://github.com/google/j2objc/](https://github.com/google/j2objc/) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) @@ -334,6 +338,14 @@ * **Project URL:** [https://github.com/kotest/kotest](https://github.com/kotest/kotest) * **License:** [Apache-2.0](https://opensource.org/licenses/Apache-2.0) +1. **Group** : io.opentelemetry. **Name** : opentelemetry-api. **Version** : 1.41.0. + * **Project URL:** [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : io.opentelemetry. **Name** : opentelemetry-context. **Version** : 1.41.0. + * **Project URL:** [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : javax.inject. **Name** : javax.inject. **Version** : 1. * **Project URL:** [http://code.google.com/p/atinject/](http://code.google.com/p/atinject/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -384,6 +396,23 @@ * **Project URL:** [https://github.com/apiguardian-team/apiguardian](https://github.com/apiguardian-team/apiguardian) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.bouncycastle. **Name** : bcpg-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcpkix-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcprov-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcutil-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + 1. **Group** : org.checkerframework. **Name** : checker-compat-qual. **Version** : 2.5.3. * **Project URL:** [https://checkerframework.org](https://checkerframework.org) * **License:** [GNU General Public License, version 2 (GPL2), with the classpath exception](http://www.gnu.org/software/classpath/license.html) @@ -473,31 +502,23 @@ * **Project URL:** [https://github.com/JetBrains/intellij-deps-trove4j](https://github.com/JetBrains/intellij-deps-trove4j) * **License:** [GNU LESSER GENERAL PUBLIC LICENSE 2.1](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html) -1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools-api. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools-api. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-api. **Version** : 2.3.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-compat. **Version** : 2.3.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-cri-impl. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-api. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-impl. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-impl. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -505,15 +526,15 @@ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-runner. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-runner. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-client. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-client. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -521,19 +542,15 @@ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-abi-reader. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-commonizer-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-commonizer-embeddable. **Version** : 2.3.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-metadata-jvm. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-metadata-jvm. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -541,7 +558,7 @@ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -549,23 +566,23 @@ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-script-runtime. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-script-runtime. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-common. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-common. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-impl-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-impl-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-jvm. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-jvm. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -573,7 +590,7 @@ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -581,7 +598,7 @@ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-common. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-common. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -589,7 +606,7 @@ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk7. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk7. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -597,14 +614,18 @@ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk8. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk8. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-tooling-core. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : swift-export-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : atomicfu. **Version** : 0.23.1. + * **Project URL:** [https://github.com/Kotlin/kotlinx.atomicfu](https://github.com/Kotlin/kotlinx.atomicfu) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : atomicfu. **Version** : 0.29.0. * **Project URL:** [https://github.com/Kotlin/kotlinx.atomicfu](https://github.com/Kotlin/kotlinx.atomicfu) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -657,14 +678,26 @@ * **Project URL:** [https://github.com/Kotlin/kotlinx.html](https://github.com/Kotlin/kotlinx.html) * **License:** [The Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-bom. **Version** : 1.7.3. + * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core. **Version** : 1.7.3. + * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core-jvm. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core-jvm. **Version** : 1.7.3. + * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-json. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -681,7 +714,7 @@ * **Project URL:** [http://jspecify.org/](http://jspecify.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.junit. **Name** : junit-bom. **Version** : 6.0.3. +1. **Group** : org.junit. **Name** : junit-bom. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) @@ -689,27 +722,27 @@ * **Project URL:** [https://junit-pioneer.org/](https://junit-pioneer.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-api. **Version** : 6.0.3. +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-api. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-engine. **Version** : 6.0.3. +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-engine. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-params. **Version** : 6.0.3. +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-params. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-commons. **Version** : 6.0.3. +1. **Group** : org.junit.platform. **Name** : junit-platform-commons. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-engine. **Version** : 6.0.3. +1. **Group** : org.junit.platform. **Name** : junit-platform-engine. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-launcher. **Version** : 6.0.3. +1. **Group** : org.junit.platform. **Name** : junit-platform-launcher. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) @@ -764,14 +797,14 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using +This report was generated on **Mon Dec 22 14:00:41 WET 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-base:2.0.0-SNAPSHOT.387` +# Dependencies of `io.spine:spine-base:2.0.0-SNAPSHOT.384` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -801,15 +834,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/google/j2objc/](https://github.com/google/j2objc/) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) @@ -817,15 +850,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -877,6 +910,10 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : com.github.ben-manes.caffeine. **Name** : caffeine. **Version** : 2.9.3. + * **Project URL:** [https://github.com/ben-manes/caffeine](https://github.com/ben-manes/caffeine) + * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : com.github.ben-manes.caffeine. **Name** : caffeine. **Version** : 3.0.5. * **Project URL:** [https://github.com/ben-manes/caffeine](https://github.com/ben-manes/caffeine) * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -909,11 +946,11 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/google/gson](https://github.com/google/gson) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.devtools.ksp. **Name** : symbol-processing. **Version** : 2.3.6. +1. **Group** : com.google.devtools.ksp. **Name** : symbol-processing. **Version** : 2.3.0. * **Project URL:** [https://goo.gle/ksp](https://goo.gle/ksp) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.devtools.ksp. **Name** : symbol-processing-api. **Version** : 2.3.6. +1. **Group** : com.google.devtools.ksp. **Name** : symbol-processing-api. **Version** : 2.3.0. * **Project URL:** [https://goo.gle/ksp](https://goo.gle/ksp) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -970,19 +1007,19 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/google/j2objc/](https://github.com/google/j2objc/) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protoc. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protoc. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1178,6 +1215,14 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/kotest/kotest](https://github.com/kotest/kotest) * **License:** [Apache-2.0](https://opensource.org/licenses/Apache-2.0) +1. **Group** : io.opentelemetry. **Name** : opentelemetry-api. **Version** : 1.41.0. + * **Project URL:** [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : io.opentelemetry. **Name** : opentelemetry-context. **Version** : 1.41.0. + * **Project URL:** [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : javax.inject. **Name** : javax.inject. **Version** : 1. * **Project URL:** [http://code.google.com/p/atinject/](http://code.google.com/p/atinject/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1236,6 +1281,23 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/apiguardian-team/apiguardian](https://github.com/apiguardian-team/apiguardian) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.bouncycastle. **Name** : bcpg-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcpkix-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcprov-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcutil-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + 1. **Group** : org.checkerframework. **Name** : checker-compat-qual. **Version** : 2.5.3. * **Project URL:** [https://checkerframework.org](https://checkerframework.org) * **License:** [GNU General Public License, version 2 (GPL2), with the classpath exception](http://www.gnu.org/software/classpath/license.html) @@ -1325,31 +1387,23 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/JetBrains/intellij-deps-trove4j](https://github.com/JetBrains/intellij-deps-trove4j) * **License:** [GNU LESSER GENERAL PUBLIC LICENSE 2.1](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html) -1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools-api. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools-api. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-api. **Version** : 2.3.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-compat. **Version** : 2.3.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-cri-impl. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-api. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-impl. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-impl. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1357,15 +1411,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-runner. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-runner. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-client. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-client. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1373,19 +1427,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-abi-reader. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-commonizer-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-commonizer-embeddable. **Version** : 2.3.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-metadata-jvm. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-metadata-jvm. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1393,7 +1443,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1401,23 +1451,23 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-script-runtime. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-script-runtime. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-common. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-common. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-impl-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-impl-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-jvm. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-jvm. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1425,7 +1475,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1433,7 +1483,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-common. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-common. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1441,7 +1491,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk7. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk7. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1449,14 +1499,18 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk8. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk8. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-tooling-core. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : swift-export-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : atomicfu. **Version** : 0.23.1. + * **Project URL:** [https://github.com/Kotlin/kotlinx.atomicfu](https://github.com/Kotlin/kotlinx.atomicfu) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : atomicfu. **Version** : 0.29.0. * **Project URL:** [https://github.com/Kotlin/kotlinx.atomicfu](https://github.com/Kotlin/kotlinx.atomicfu) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1509,14 +1563,26 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/Kotlin/kotlinx.html](https://github.com/Kotlin/kotlinx.html) * **License:** [The Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-bom. **Version** : 1.7.3. + * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core. **Version** : 1.7.3. + * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core-jvm. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core-jvm. **Version** : 1.7.3. + * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-json. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1533,7 +1599,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [http://jspecify.org/](http://jspecify.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.junit. **Name** : junit-bom. **Version** : 6.0.3. +1. **Group** : org.junit. **Name** : junit-bom. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) @@ -1541,27 +1607,27 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://junit-pioneer.org/](https://junit-pioneer.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-api. **Version** : 6.0.3. +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-api. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-engine. **Version** : 6.0.3. +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-engine. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-params. **Version** : 6.0.3. +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-params. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-commons. **Version** : 6.0.3. +1. **Group** : org.junit.platform. **Name** : junit-platform-commons. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-engine. **Version** : 6.0.3. +1. **Group** : org.junit.platform. **Name** : junit-platform-engine. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-launcher. **Version** : 6.0.3. +1. **Group** : org.junit.platform. **Name** : junit-platform-launcher. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) @@ -1616,14 +1682,14 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using +This report was generated on **Mon Dec 22 14:00:42 WET 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-environment:2.0.0-SNAPSHOT.387` +# Dependencies of `io.spine:spine-environment:2.0.0-SNAPSHOT.384` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -1653,15 +1719,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/google/j2objc/](https://github.com/google/j2objc/) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) @@ -1669,15 +1735,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1740,6 +1806,10 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : com.github.ben-manes.caffeine. **Name** : caffeine. **Version** : 2.9.3. + * **Project URL:** [https://github.com/ben-manes/caffeine](https://github.com/ben-manes/caffeine) + * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : com.github.ben-manes.caffeine. **Name** : caffeine. **Version** : 3.0.5. * **Project URL:** [https://github.com/ben-manes/caffeine](https://github.com/ben-manes/caffeine) * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1825,15 +1895,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/google/j2objc/](https://github.com/google/j2objc/) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) @@ -2016,6 +2086,14 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/kotest/kotest](https://github.com/kotest/kotest) * **License:** [Apache-2.0](https://opensource.org/licenses/Apache-2.0) +1. **Group** : io.opentelemetry. **Name** : opentelemetry-api. **Version** : 1.41.0. + * **Project URL:** [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : io.opentelemetry. **Name** : opentelemetry-context. **Version** : 1.41.0. + * **Project URL:** [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : javax.inject. **Name** : javax.inject. **Version** : 1. * **Project URL:** [http://code.google.com/p/atinject/](http://code.google.com/p/atinject/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2066,6 +2144,23 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/apiguardian-team/apiguardian](https://github.com/apiguardian-team/apiguardian) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.bouncycastle. **Name** : bcpg-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcpkix-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcprov-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcutil-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + 1. **Group** : org.checkerframework. **Name** : checker-compat-qual. **Version** : 2.5.3. * **Project URL:** [https://checkerframework.org](https://checkerframework.org) * **License:** [GNU General Public License, version 2 (GPL2), with the classpath exception](http://www.gnu.org/software/classpath/license.html) @@ -2155,31 +2250,23 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/JetBrains/intellij-deps-trove4j](https://github.com/JetBrains/intellij-deps-trove4j) * **License:** [GNU LESSER GENERAL PUBLIC LICENSE 2.1](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html) -1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools-api. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools-api. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-api. **Version** : 2.3.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-compat. **Version** : 2.3.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-cri-impl. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-api. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-impl. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-impl. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2187,15 +2274,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-runner. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-runner. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-client. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-client. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2203,19 +2290,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-abi-reader. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-commonizer-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-commonizer-embeddable. **Version** : 2.3.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-metadata-jvm. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-metadata-jvm. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2223,7 +2306,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2231,23 +2314,23 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-script-runtime. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-script-runtime. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-common. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-common. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-impl-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-impl-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-jvm. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-jvm. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2255,7 +2338,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2263,7 +2346,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-common. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-common. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2271,7 +2354,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk7. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk7. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2279,14 +2362,18 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk8. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk8. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-tooling-core. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : swift-export-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : atomicfu. **Version** : 0.23.1. + * **Project URL:** [https://github.com/Kotlin/kotlinx.atomicfu](https://github.com/Kotlin/kotlinx.atomicfu) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : atomicfu. **Version** : 0.29.0. * **Project URL:** [https://github.com/Kotlin/kotlinx.atomicfu](https://github.com/Kotlin/kotlinx.atomicfu) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2339,14 +2426,26 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/Kotlin/kotlinx.html](https://github.com/Kotlin/kotlinx.html) * **License:** [The Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-bom. **Version** : 1.7.3. + * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core. **Version** : 1.7.3. + * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core-jvm. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core-jvm. **Version** : 1.7.3. + * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-json. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2363,7 +2462,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [http://jspecify.org/](http://jspecify.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.junit. **Name** : junit-bom. **Version** : 6.0.3. +1. **Group** : org.junit. **Name** : junit-bom. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) @@ -2371,27 +2470,27 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://junit-pioneer.org/](https://junit-pioneer.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-api. **Version** : 6.0.3. +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-api. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-engine. **Version** : 6.0.3. +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-engine. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-params. **Version** : 6.0.3. +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-params. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-commons. **Version** : 6.0.3. +1. **Group** : org.junit.platform. **Name** : junit-platform-commons. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-engine. **Version** : 6.0.3. +1. **Group** : org.junit.platform. **Name** : junit-platform-engine. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-launcher. **Version** : 6.0.3. +1. **Group** : org.junit.platform. **Name** : junit-platform-launcher. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) @@ -2446,14 +2545,14 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using +This report was generated on **Mon Dec 22 14:00:41 WET 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-format:2.0.0-SNAPSHOT.387` +# Dependencies of `io.spine:spine-format:2.0.0-SNAPSHOT.384` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.20.0. @@ -2527,15 +2626,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/google/j2objc/](https://github.com/google/j2objc/) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) @@ -2543,15 +2642,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2646,6 +2745,10 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : com.github.ben-manes.caffeine. **Name** : caffeine. **Version** : 2.9.3. + * **Project URL:** [https://github.com/ben-manes/caffeine](https://github.com/ben-manes/caffeine) + * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : com.github.ben-manes.caffeine. **Name** : caffeine. **Version** : 3.0.5. * **Project URL:** [https://github.com/ben-manes/caffeine](https://github.com/ben-manes/caffeine) * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2731,15 +2834,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/google/j2objc/](https://github.com/google/j2objc/) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.34.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.33.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) @@ -2922,6 +3025,14 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/kotest/kotest](https://github.com/kotest/kotest) * **License:** [Apache-2.0](https://opensource.org/licenses/Apache-2.0) +1. **Group** : io.opentelemetry. **Name** : opentelemetry-api. **Version** : 1.41.0. + * **Project URL:** [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : io.opentelemetry. **Name** : opentelemetry-context. **Version** : 1.41.0. + * **Project URL:** [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : javax.inject. **Name** : javax.inject. **Version** : 1. * **Project URL:** [http://code.google.com/p/atinject/](http://code.google.com/p/atinject/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2972,6 +3083,23 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/apiguardian-team/apiguardian](https://github.com/apiguardian-team/apiguardian) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.bouncycastle. **Name** : bcpg-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcpkix-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcprov-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcutil-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + 1. **Group** : org.checkerframework. **Name** : checker-compat-qual. **Version** : 2.5.3. * **Project URL:** [https://checkerframework.org](https://checkerframework.org) * **License:** [GNU General Public License, version 2 (GPL2), with the classpath exception](http://www.gnu.org/software/classpath/license.html) @@ -3061,31 +3189,23 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/JetBrains/intellij-deps-trove4j](https://github.com/JetBrains/intellij-deps-trove4j) * **License:** [GNU LESSER GENERAL PUBLIC LICENSE 2.1](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html) -1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools-api. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools-api. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-api. **Version** : 2.3.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-compat. **Version** : 2.3.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-cri-impl. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-api. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-impl. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-impl. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3093,15 +3213,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-runner. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-runner. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-client. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-client. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3109,19 +3229,15 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-abi-reader. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-commonizer-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-commonizer-embeddable. **Version** : 2.3.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-metadata-jvm. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-metadata-jvm. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3129,7 +3245,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3137,23 +3253,23 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-script-runtime. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-script-runtime. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-common. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-common. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-impl-embeddable. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-impl-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-jvm. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-jvm. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3161,7 +3277,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3169,7 +3285,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-common. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-common. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3177,7 +3293,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk7. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk7. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3185,14 +3301,18 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk8. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk8. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-tooling-core. **Version** : 2.3.20. +1. **Group** : org.jetbrains.kotlin. **Name** : swift-export-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : atomicfu. **Version** : 0.23.1. + * **Project URL:** [https://github.com/Kotlin/kotlinx.atomicfu](https://github.com/Kotlin/kotlinx.atomicfu) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : atomicfu. **Version** : 0.29.0. * **Project URL:** [https://github.com/Kotlin/kotlinx.atomicfu](https://github.com/Kotlin/kotlinx.atomicfu) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3245,14 +3365,26 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://github.com/Kotlin/kotlinx.html](https://github.com/Kotlin/kotlinx.html) * **License:** [The Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-bom. **Version** : 1.7.3. + * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core. **Version** : 1.7.3. + * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core-jvm. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core-jvm. **Version** : 1.7.3. + * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-json. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3269,7 +3401,7 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [http://jspecify.org/](http://jspecify.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.junit. **Name** : junit-bom. **Version** : 6.0.3. +1. **Group** : org.junit. **Name** : junit-bom. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) @@ -3277,27 +3409,27 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using * **Project URL:** [https://junit-pioneer.org/](https://junit-pioneer.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-api. **Version** : 6.0.3. +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-api. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-engine. **Version** : 6.0.3. +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-engine. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-params. **Version** : 6.0.3. +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-params. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-commons. **Version** : 6.0.3. +1. **Group** : org.junit.platform. **Name** : junit-platform-commons. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-engine. **Version** : 6.0.3. +1. **Group** : org.junit.platform. **Name** : junit-platform-engine. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-launcher. **Version** : 6.0.3. +1. **Group** : org.junit.platform. **Name** : junit-platform-launcher. **Version** : 6.0.0. * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) @@ -3356,6 +3488,6 @@ This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri Apr 03 16:59:29 WEST 2026** using +This report was generated on **Mon Dec 22 14:00:41 WET 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under -[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file +[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md index 8c92a264b1..14cc57c8a4 100644 --- a/docs/dependencies/dependencies.md +++ b/docs/dependencies/dependencies.md @@ -1,6 +1,6 @@ -# Dependencies of `io.spine:spine-annotations:2.0.0-SNAPSHOT.391` +# Dependencies of `io.spine:spine-annotations:2.0.0-SNAPSHOT.392` ## Runtime 1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.0.2. @@ -764,14 +764,14 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu May 28 20:47:29 WEST 2026** using +This report was generated on **Mon Jun 01 10:59:29 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-base:2.0.0-SNAPSHOT.391` +# Dependencies of `io.spine:spine-base:2.0.0-SNAPSHOT.392` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -1616,14 +1616,14 @@ This report was generated on **Thu May 28 20:47:29 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu May 28 20:47:29 WEST 2026** using +This report was generated on **Mon Jun 01 10:59:29 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-environment:2.0.0-SNAPSHOT.391` +# Dependencies of `io.spine:spine-environment:2.0.0-SNAPSHOT.392` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -2446,14 +2446,14 @@ This report was generated on **Thu May 28 20:47:29 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu May 28 20:47:29 WEST 2026** using +This report was generated on **Mon Jun 01 10:59:29 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-format:2.0.0-SNAPSHOT.391` +# Dependencies of `io.spine:spine-format:2.0.0-SNAPSHOT.392` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.21.3. @@ -3356,6 +3356,6 @@ This report was generated on **Thu May 28 20:47:29 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu May 28 20:47:30 WEST 2026** using +This report was generated on **Mon Jun 01 10:59:29 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file diff --git a/docs/dependencies/pom.xml b/docs/dependencies/pom.xml index e0550a2b31..f7f612abab 100644 --- a/docs/dependencies/pom.xml +++ b/docs/dependencies/pom.xml @@ -10,7 +10,7 @@ all modules and does not describe the project structure per-subproject. --> io.spine base-libraries -2.0.0-SNAPSHOT.391 +2.0.0-SNAPSHOT.392 2015 From 0957e857ff7c2fcc0b524486e8fafd27f7b6f91c Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Mon, 1 Jun 2026 12:13:31 +0100 Subject: [PATCH 04/45] Instruct the `raise-coverage` to use `EqualsTester` when applicable --- .agents/skills/raise-coverage/SKILL.md | 12 ++++++++++++ .agents/skills/raise-coverage/agents/openai.yaml | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.agents/skills/raise-coverage/SKILL.md b/.agents/skills/raise-coverage/SKILL.md index 790007056e..2f921eab04 100644 --- a/.agents/skills/raise-coverage/SKILL.md +++ b/.agents/skills/raise-coverage/SKILL.md @@ -188,6 +188,18 @@ On success, **resume** at Workflow step 1. `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. + - **For `equals()` / `hashCode()` contracts, use Guava's `EqualsTester`.** + When the target includes `equals(Object)` or `hashCode()`, write the test + with `com.google.common.testing.EqualsTester` (from `guava-testlib`, + already on the test classpath in Spine repos) instead of hand-rolled + `shouldBe` assertions. Build equality groups with `.addEqualityGroup(...)` + — at least two groups, each holding two or more instances — and finish + with `.testEquals()`. A single call exercises reflexivity, symmetry, + transitivity within a group, the `null` branch, type mismatch (against + `Object`), and `hashCode` consistency, which closes the typical + `equals`/`hashCode` gaps in one assertion. Use Kotest matchers only for + properties `EqualsTester` does not cover (e.g. a custom `toString()` + contract). - **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 diff --git a/.agents/skills/raise-coverage/agents/openai.yaml b/.agents/skills/raise-coverage/agents/openai.yaml index 32a4ed1f9f..932676c41e 100644 --- a/.agents/skills/raise-coverage/agents/openai.yaml +++ b/.agents/skills/raise-coverage/agents/openai.yaml @@ -1,4 +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." + 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`; whenever the target covers `equals(Object)` or `hashCode()`, write the test with Guava's `EqualsTester` from `guava-testlib` — build equality groups and call `.testEquals()` — instead of hand-rolled `shouldBe` assertions) and re-run the same report task to verify the gaps are closed. Tests-only changes do not require a version bump." From f79d46b8f1ceaea0dfbb7f90e70a8cdf57b8afc0 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Mon, 1 Jun 2026 12:13:48 +0100 Subject: [PATCH 05/45] Migrate to Kover --- build.gradle.kts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index f194f9d811..170a768375 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,11 +1,11 @@ /* - * Copyright 2023, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -32,7 +32,7 @@ import io.spine.dependency.local.Logging import io.spine.gradle.publish.PublishingRepos import io.spine.gradle.publish.spinePublishing import io.spine.gradle.repo.standardToSpineSdk -import io.spine.gradle.report.coverage.JacocoConfig +import io.spine.gradle.report.coverage.KoverConfig import io.spine.gradle.report.license.LicenseReporter import io.spine.gradle.report.pom.PomGenerator @@ -46,7 +46,6 @@ buildscript { plugins { kotlin - jacoco `gradle-doctor` `project-report` `dokka-setup` @@ -113,8 +112,9 @@ val dokkaGeneratePublicationHtml by tasks.getting { dependsOn(tasks.jar) } +KoverConfig.applyTo(project) + gradle.projectsEvaluated { - JacocoConfig.applyTo(project) LicenseReporter.mergeAllReports(project) PomGenerator.applyTo(project) } From a900663ce29ed7bdfd7662d26fded40f1f250eb2 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Mon, 1 Jun 2026 12:14:23 +0100 Subject: [PATCH 06/45] Test `EnvironmentType` equality and `hashCode()` --- .../spine/environment/EnvironmentTypeSpec.kt | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 environment/src/test/kotlin/io/spine/environment/EnvironmentTypeSpec.kt diff --git a/environment/src/test/kotlin/io/spine/environment/EnvironmentTypeSpec.kt b/environment/src/test/kotlin/io/spine/environment/EnvironmentTypeSpec.kt new file mode 100644 index 0000000000..b6ac0b4fa1 --- /dev/null +++ b/environment/src/test/kotlin/io/spine/environment/EnvironmentTypeSpec.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.environment + +import com.google.common.testing.EqualsTester +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`EnvironmentType` should") +internal class EnvironmentTypeSpec { + + @Test + fun `obey the 'equals' and 'hashCode' contract`() { + EqualsTester() + .addEqualityGroup(StubType(), StubType()) + .addEqualityGroup(OtherStubType(), OtherStubType()) + .testEquals() + } +} + +private class StubType : EnvironmentType() { + override fun enabled(): Boolean = false + override fun self(): StubType = this +} + +private class OtherStubType : EnvironmentType() { + override fun enabled(): Boolean = false + override fun self(): OtherStubType = this +} From 80a89345541bc0aabed10991fc056254bbab00c8 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Mon, 1 Jun 2026 12:18:54 +0100 Subject: [PATCH 07/45] Remove redundant skill instruction --- .agents/skills/raise-coverage/SKILL.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.agents/skills/raise-coverage/SKILL.md b/.agents/skills/raise-coverage/SKILL.md index 2f921eab04..169c6dc2df 100644 --- a/.agents/skills/raise-coverage/SKILL.md +++ b/.agents/skills/raise-coverage/SKILL.md @@ -197,9 +197,7 @@ On success, **resume** at Workflow step 1. with `.testEquals()`. A single call exercises reflexivity, symmetry, transitivity within a group, the `null` branch, type mismatch (against `Object`), and `hashCode` consistency, which closes the typical - `equals`/`hashCode` gaps in one assertion. Use Kotest matchers only for - properties `EqualsTester` does not cover (e.g. a custom `toString()` - contract). + `equals`/`hashCode` gaps in one assertion. - **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 From 5626d34caab0061a019bc2796af6a653147ebed5 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Mon, 1 Jun 2026 12:22:02 +0100 Subject: [PATCH 08/45] Add meta-data for Codex agents Also: * Add `source-command-build-run-build` --- .../skills/source-command-run-build/SKILL.md | 18 ++++++++ .codex/agents/dependency-audit.toml | 15 +++++++ .codex/agents/kotlin-review.toml | 13 ++++++ .codex/agents/review-docs.toml | 14 ++++++ .codex/hooks.json | 43 +++++++++++++++++++ 5 files changed, 103 insertions(+) create mode 100644 .agents/skills/source-command-run-build/SKILL.md create mode 100644 .codex/agents/dependency-audit.toml create mode 100644 .codex/agents/kotlin-review.toml create mode 100644 .codex/agents/review-docs.toml create mode 100644 .codex/hooks.json diff --git a/.agents/skills/source-command-run-build/SKILL.md b/.agents/skills/source-command-run-build/SKILL.md new file mode 100644 index 0000000000..c462d4dd9b --- /dev/null +++ b/.agents/skills/source-command-run-build/SKILL.md @@ -0,0 +1,18 @@ +--- +name: "source-command-run-build" +description: "Build the project the right way based on what changed (proto vs. Kotlin/Java vs. docs)." +--- + +# source-command-run-build + +Use this skill when the user asks to run the migrated source command `run-build`. + +## Command Template + +Decide which build to run by looking at `git status --short` and `git diff --name-only`: + +- If any `.proto` files changed: `./gradlew clean build` +- Else if Kotlin or Java source changed: `./gradlew build` +- Else if only docs/comments changed (KDoc / Javadoc / Markdown): `./gradlew dokka`. Tests are NOT required for doc-only changes. + +Report the chosen command and its result. See `.agents/running-builds.md`. diff --git a/.codex/agents/dependency-audit.toml b/.codex/agents/dependency-audit.toml new file mode 100644 index 0000000000..09518dd56c --- /dev/null +++ b/.codex/agents/dependency-audit.toml @@ -0,0 +1,15 @@ +name = "dependency-audit" +description = 'Audits changes to dependency declarations under `buildSrc/src/main/kotlin/io/spine/dependency/` — catches accidental version downgrades, BOM mismatches, missing deprecation markers, copyright drift, and convention drift. Use proactively whenever a diff touches that directory, or when the user asks "audit this dependency bump". Read-only; does not run builds.' +developer_instructions = """ +Follow the `dependency-audit` skill exactly: + +- Skill: `.agents/skills/dependency-audit/SKILL.md` +- The skill owns the per-area checks (version sanity, naming and structure, + deprecation discipline, convention drift, cross-cutting) and the output + format (Must fix / Should fix / Nits + one-line verdict). +- Read-only: use `Read`, `Grep`, `Glob`, and `Bash` solely for `git diff`, + `git grep`, and related read-only inspection. Do not run builds. +- **Be fast.** Fetch the full unified diff once, work from it, and `Read` + individual files only when the skill's step 2 budget allows. Issue + independent `Grep`/`Bash` calls in parallel within a single response; + do not halt at the first failure — collect all findings and report once.""" diff --git a/.codex/agents/kotlin-review.toml b/.codex/agents/kotlin-review.toml new file mode 100644 index 0000000000..56e491b856 --- /dev/null +++ b/.codex/agents/kotlin-review.toml @@ -0,0 +1,13 @@ +name = "kotlin-review" +description = "Reviews Kotlin (and Java) changes against Spine coding guidelines, safety rules, and testing policy. Use proactively after any non-trivial code edit, before opening a PR, or when the user asks for a code review. Read-only; does not run builds." +developer_instructions = """ +Follow the `kotlin-review` skill exactly: + +- Skill: `.agents/skills/kotlin-review/SKILL.md` +- The skill owns the procedure, the checks (Kotlin idioms, safety rules, + testing policy, version-gate applicability), and the output format + (Must fix / Should fix / Nits + one-line verdict). +- Stay in scope: code only. If a documentation issue surfaces, note it + briefly as a Nit pointing at the `review-docs` agent. +- Read-only: use `Read`, `Grep`, `Glob`, and `Bash` solely for `git diff` + and related read-only inspection. Do not run builds.""" diff --git a/.codex/agents/review-docs.toml b/.codex/agents/review-docs.toml new file mode 100644 index 0000000000..6ebbd6ad7c --- /dev/null +++ b/.codex/agents/review-docs.toml @@ -0,0 +1,14 @@ +name = "review-docs" +description = "Reviews documentation changes — KDoc/Javadoc inside Kotlin/Java sources and Markdown docs (`README.md`, `docs/**`) — against Spine documentation conventions. Use proactively when a diff touches doc comments or Markdown, before opening a doc-affecting PR, or when the user asks for a documentation review. Read-only; does not run builds." +developer_instructions = """ +Follow the `review-docs` skill exactly: + +- Skill: `.agents/skills/review-docs/SKILL.md` +- The skill owns the review procedure, the per-area checks (KDoc/Javadoc, + Markdown, prose flow, terminology), and the output format + (Must fix / Should fix / Nits + one-line verdict). +- Scope yourself to documentation only. If you spot a code-quality issue, + surface it briefly as a Nit pointing at the `kotlin-review` agent — + do not expand the review. +- Read-only: use `Read`, `Grep`, `Glob`, and `Bash` solely for `git diff` + and related read-only inspection. Do not run builds.""" diff --git a/.codex/hooks.json b/.codex/hooks.json new file mode 100644 index 0000000000..b7e5c49296 --- /dev/null +++ b/.codex/hooks.json @@ -0,0 +1,43 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Edit|Write|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/protect-version-file.sh" + } + ] + }, + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/pre-pr-gate.sh" + }, + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/publish-version-gate.sh" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Edit|Write|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/sanitize-source-code.sh" + }, + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/update-copyright.sh" + } + ] + } + ] + } +} From 9253243ea7e0a0677c09b9ead0ca66b085d3ad34 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Mon, 1 Jun 2026 12:45:40 +0100 Subject: [PATCH 09/45] Add plan document --- .agents/tasks/raise-base-coverage.md | 141 +++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 .agents/tasks/raise-base-coverage.md diff --git a/.agents/tasks/raise-base-coverage.md b/.agents/tasks/raise-base-coverage.md new file mode 100644 index 0000000000..49062edf17 --- /dev/null +++ b/.agents/tasks/raise-base-coverage.md @@ -0,0 +1,141 @@ +--- +slug: raise-base-coverage +branch: increse-coverage +owner: codex +status: in-progress +started: 2026-06-01 +related-memories: [] +--- + +## Goal + +Raise test coverage for the `:base` module using the `raise-coverage` +workflow. Success means Kover report gaps are localized, concrete test cases +are approved before implementation when actionable gaps exist, tests are added +in Kotlin with Kotest assertions and no mocks, and the follow-up Kover report +confirms targeted gaps closed without weakening Codecov settings. + +## Context + +- Target module: `:base`. +- Coverage source: `base/build/reports/kover/report.xml`. +- Kover is applied via the shared `module` plugin and root `KoverConfig`. +- Tests-only changes do not require a version bump. +- The skill requires an approval pause after proposing test cases. + +## Plan + +- [x] Load repo orientation, memory, testing rules, and coverage references. +- [x] Confirm Kover is already available for the target module. +- [x] Generate and parse the `:base` Kover XML report. +- [x] Read target sources and existing tests for selected gaps. +- [x] Document proposed concrete test cases for approval. +- [ ] Wait for approval to write tests. +- [ ] Add approved Kotlin `*Spec` tests using stubs, not mocks. +- [ ] Re-run `:base:koverXmlReport` and confirm targeted gaps closed. + +## Findings + +Step 0 found no migration work. Kover is already available through the shared +`module` plugin and root `KoverConfig`; no Gradle module inspected applies +vanilla JaCoCo. + +Generated report: + +- Command: `./gradlew :base:koverXmlReport --quiet`. +- XML: `base/build/reports/kover/report.xml`. +- Report format: JaCoCo XML emitted by Kover, with `report.dtd` DOCTYPE. +- Module totals: 3265/4212 lines covered (77.52%) and 750/1087 branches + covered (69.00%). + +Selected actionable gaps: + +- `base/src/main/java/io/spine/code/fs/FsObject.java` — lines `53`, `64`, + `71`, `77`, `82`, `87`, `92-99`; branches in `equals()`. +- `base/src/main/java/io/spine/code/fs/AbstractSourceFile.java` — lines + `63-71`, `78-84`, `92-100`; branches in `lines()`. +- `base/src/main/java/io/spine/code/fs/SourceCodeDirectory.java` — lines + `44-56`. +- `base/src/main/java/io/spine/code/fs/AbstractDirectory.java` — constructor + lines `40-41`, covered by instantiating a concrete test subclass. +- `base/src/main/kotlin/io/spine/io/Files.kt` — lines `57-61`, `70-73`; + branches in `File.toUnix()` and `File.toUnixPath()`. +- `base/src/main/kotlin/io/spine/io/Paths.kt` — lines `66-70`; branches in + `Path.toUnix()`. + +Non-actionable note: + +- `AbstractSourceFile.java` lines `69` and `82` are `throw helper(...)` lines + where the helper throws internally. They may remain as JaCoCo gaps even when + the surrounding `IOException` paths are exercised; this is documented in the + `raise-coverage` coverage-signals reference. + +## Proposed Cases + +Add `base/src/test/kotlin/io/spine/code/fs/FsObjectSpec.kt`: + +- `FsObject` exposes path, parent, deprecated `directory()`, existence, and + `toString()`. + Input: a temp file and a missing sibling path. + Expected: path and parent match, `directory()` delegates to `parent()`, + the real file exists, the missing file does not, and `toString()` returns + the path string. + Closes: `FsObject.java` lines `53`, `64`, `71`, `77`, and `82`. + +- `FsObject` equality and hash code are path-based. + Input: two simple concrete subclasses with the same path, and another + equality group with a different path. + Expected: `EqualsTester().addEqualityGroup(...).testEquals()`. + Closes: `FsObject.java` lines `87`, `92-99`, and the `equals()` branches. + +- `SourceCodeDirectory` resolves child directories and source files. + Input: tiny hand-written subclasses of `SourceCodeDirectory` and + `AbstractSourceFile`. + Expected: `root.resolve(child)` and `root.resolve(file)` return the resolved + `Path`. + Closes: `AbstractDirectory.java` constructor lines and + `SourceCodeDirectory.java` lines `44-56`. + +- `AbstractSourceFile` loads, exposes, updates, and stores lines. + Input: a temp text file with two lines. + Expected: `lines()` is empty before `load()`, loaded lines match file + content, `update(...)` replaces the in-memory lines, and `store()` rewrites + the file. + Closes: `AbstractSourceFile.java` lines `63-68`, `78-80`, `92-100`, and both + `lines()` branches. + +- `AbstractSourceFile.load()` rejects a missing file. + Input: non-existent temp path. + Expected: `shouldThrow`. + Closes: the precondition path at `AbstractSourceFile.java:64`. + +Extend `base/src/test/kotlin/io/spine/io/FilesSpec.kt`: + +- `File.toUnix()` converts Windows-style separators and returns the same + instance for Unix-style paths. + Closes: `Files.kt` lines `57-61`. + +- `File.toUnixPath()` converts Windows-style separators and returns the + original path string otherwise. + Closes: `Files.kt` lines `70-73`. + +Extend `base/src/test/kotlin/io/spine/io/PathsSpec.kt`: + +- `Path.toUnix()` converts Windows-style separators and returns the same + instance for Unix-style paths. + Closes: `Paths.kt` lines `66-70`. + +All proposed tests are Kotlin `*Spec` tests, use Kotest assertions, and rely on +small hand-written subclasses rather than mocks. + +## Log + +- 2026-06-01 12:29 WEST — created task file; Kover is applied through + `module` / `KoverConfig`, with no vanilla JaCoCo applied by the Gradle + modules under inspection. +- 2026-06-01 12:30 WEST — generated `:base` Kover XML report. Module totals: + 3265/4212 lines (77.52%) and 750/1087 branches (69.00%). Selected focused, + actionable gaps in `io.spine.code.fs` plus `File`/`Path` Unix conversion + extension branches for the approval proposal. +- 2026-06-01 12:32 WEST — recorded the findings and proposed test cases in + this plan document. Awaiting approval before writing tests. From 8913832ce0ca965a3caaa02bd0699a519b333405 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Mon, 1 Jun 2026 12:53:51 +0100 Subject: [PATCH 10/45] Update plan document with analysis tasks --- .agents/tasks/raise-base-coverage.md | 66 +++++++++++++++++++++------- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/.agents/tasks/raise-base-coverage.md b/.agents/tasks/raise-base-coverage.md index 49062edf17..d44bc6fe8f 100644 --- a/.agents/tasks/raise-base-coverage.md +++ b/.agents/tasks/raise-base-coverage.md @@ -9,19 +9,24 @@ related-memories: [] ## Goal -Raise test coverage for the `:base` module using the `raise-coverage` -workflow. Success means Kover report gaps are localized, concrete test cases -are approved before implementation when actionable gaps exist, tests are added -in Kotlin with Kotest assertions and no mocks, and the follow-up Kover report -confirms targeted gaps closed without weakening Codecov settings. +Raise test coverage for the `:base` module while cleaning up stale public API. +Success means deprecated `:base` API is removed, `io.spine.code.fs` usage is +checked across SpineEventEngine projects, unused `io.spine.code.fs` types are +deprecated, the remaining non-deprecated API is covered with Kotlin tests using +Kotest assertions and no mocks, and the follow-up Kover report confirms targeted +gaps closed without weakening Codecov settings. ## Context - Target module: `:base`. - Coverage source: `base/build/reports/kover/report.xml`. - Kover is applied via the shared `module` plugin and root `KoverConfig`. -- Tests-only changes do not require a version bump. +- The original coverage-only plan would not require a version bump, but the + updated scope includes production API removal/deprecation and therefore must + be treated as a production-code change. - The skill requires an approval pause after proposing test cases. +- `io.spine.code.fs` test coverage depends on the organization-wide usage + analysis: only API that remains non-deprecated should be covered. ## Plan @@ -30,10 +35,31 @@ confirms targeted gaps closed without weakening Codecov settings. - [x] Generate and parse the `:base` Kover XML report. - [x] Read target sources and existing tests for selected gaps. - [x] Document proposed concrete test cases for approval. -- [ ] Wait for approval to write tests. +- [ ] Remove deprecated API in the `:base` module. +- [ ] Analyze whether `io.spine.code.fs` is used in SpineEventEngine projects. +- [ ] Deprecate `io.spine.code.fs` types that are not used. +- [ ] Finalize test cases for the remaining non-deprecated API and wait for + approval to write tests. - [ ] Add approved Kotlin `*Spec` tests using stubs, not mocks. - [ ] Re-run `:base:koverXmlReport` and confirm targeted gaps closed. +## Updated Scope + +The plan now includes API cleanup before test generation: + +- Remove deprecated API from `:base`. The first known target from the selected + gaps is `FsObject.directory()`, which is deprecated in favor of `parent()`. + The implementation pass must scan the whole `:base` module for other + deprecated public API before editing. +- Analyze whether `io.spine.code.fs` is used by SpineEventEngine projects. + Check the current repository first, then sibling/local checkouts and GitHub + organization usage if local evidence is incomplete. +- Deprecate `io.spine.code.fs` types that have no organization usage. Do not + add coverage for API newly marked as deprecated. +- Cover only the `io.spine.code.fs` API that remains non-deprecated after the + usage analysis, plus the selected non-deprecated `File`/`Path` Unix + conversion extension branches. + ## Findings Step 0 found no migration work. Kover is already available through the shared @@ -48,10 +74,12 @@ Generated report: - Module totals: 3265/4212 lines covered (77.52%) and 750/1087 branches covered (69.00%). -Selected actionable gaps: +Selected actionable gaps before the updated API cleanup: - `base/src/main/java/io/spine/code/fs/FsObject.java` — lines `53`, `64`, - `71`, `77`, `82`, `87`, `92-99`; branches in `equals()`. + `71`, `77`, `82`, `87`, `92-99`; branches in `equals()`. Line `64` + belongs to the deprecated `directory()` method and should be removed instead + of covered. - `base/src/main/java/io/spine/code/fs/AbstractSourceFile.java` — lines `63-71`, `78-84`, `92-100`; branches in `lines()`. - `base/src/main/java/io/spine/code/fs/SourceCodeDirectory.java` — lines @@ -72,15 +100,17 @@ Non-actionable note: ## Proposed Cases +These cases are provisional until the `io.spine.code.fs` usage analysis is +complete. Cases for API that becomes deprecated must be dropped; cases for API +that remains supported should be implemented. + Add `base/src/test/kotlin/io/spine/code/fs/FsObjectSpec.kt`: -- `FsObject` exposes path, parent, deprecated `directory()`, existence, and - `toString()`. +- `FsObject` exposes path, parent, existence, and `toString()`. Input: a temp file and a missing sibling path. - Expected: path and parent match, `directory()` delegates to `parent()`, - the real file exists, the missing file does not, and `toString()` returns - the path string. - Closes: `FsObject.java` lines `53`, `64`, `71`, `77`, and `82`. + Expected: path and parent match, the real file exists, the missing file does + not, and `toString()` returns the path string. + Closes: `FsObject.java` lines `53`, `71`, `77`, and `82`. - `FsObject` equality and hash code are path-based. Input: two simple concrete subclasses with the same path, and another @@ -126,7 +156,8 @@ Extend `base/src/test/kotlin/io/spine/io/PathsSpec.kt`: Closes: `Paths.kt` lines `66-70`. All proposed tests are Kotlin `*Spec` tests, use Kotest assertions, and rely on -small hand-written subclasses rather than mocks. +small hand-written subclasses rather than mocks. Do not test removed or newly +deprecated API. ## Log @@ -139,3 +170,6 @@ small hand-written subclasses rather than mocks. extension branches for the approval proposal. - 2026-06-01 12:32 WEST — recorded the findings and proposed test cases in this plan document. Awaiting approval before writing tests. +- 2026-06-01 12:34 WEST — updated scope per user request: remove deprecated + `:base` API, analyze `io.spine.code.fs` usage across SpineEventEngine + projects, deprecate unused types, and cover only the non-deprecated API. From 231b824c07f4895a7da7b096dacc9607905ac7f6 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Mon, 1 Jun 2026 13:02:09 +0100 Subject: [PATCH 11/45] Remove deprecated API in `:base` --- .agents/tasks/raise-base-coverage.md | 43 +++- .../main/java/io/spine/base/Identifier.java | 28 +-- .../main/java/io/spine/code/fs/FsObject.java | 19 +- .../java/io/spine/code/proto/SourceFile.java | 40 +-- .../spine/protobuf/CollectionsConverter.java | 121 --------- .../java/io/spine/protobuf/Durations2.java | 40 +-- .../src/main/java/io/spine/query/Columns.java | 43 +--- base/src/main/java/io/spine/type/TypeUrl.java | 14 +- base/src/main/java/io/spine/util/Text.java | 235 ------------------ base/src/main/kotlin/io/spine/io/Files.kt | 18 +- .../main/kotlin/io/spine/protobuf/AnyExts.kt | 15 +- .../src/main/kotlin/io/spine/string/Indent.kt | 13 +- .../kotlin/io/spine/util/MoreCollections.kt | 61 ----- .../protobuf/CollectionsConverterTest.java | 96 ------- .../src/test/kotlin/io/spine/util/TextTest.kt | 88 ------- 15 files changed, 54 insertions(+), 820 deletions(-) delete mode 100644 base/src/main/java/io/spine/protobuf/CollectionsConverter.java delete mode 100644 base/src/main/java/io/spine/util/Text.java delete mode 100644 base/src/main/kotlin/io/spine/util/MoreCollections.kt delete mode 100644 base/src/test/java/io/spine/protobuf/CollectionsConverterTest.java delete mode 100644 base/src/test/kotlin/io/spine/util/TextTest.kt diff --git a/.agents/tasks/raise-base-coverage.md b/.agents/tasks/raise-base-coverage.md index d44bc6fe8f..53b86813e9 100644 --- a/.agents/tasks/raise-base-coverage.md +++ b/.agents/tasks/raise-base-coverage.md @@ -35,7 +35,7 @@ gaps closed without weakening Codecov settings. - [x] Generate and parse the `:base` Kover XML report. - [x] Read target sources and existing tests for selected gaps. - [x] Document proposed concrete test cases for approval. -- [ ] Remove deprecated API in the `:base` module. +- [x] Remove deprecated API in the `:base` module. - [ ] Analyze whether `io.spine.code.fs` is used in SpineEventEngine projects. - [ ] Deprecate `io.spine.code.fs` types that are not used. - [ ] Finalize test cases for the remaining non-deprecated API and wait for @@ -87,7 +87,9 @@ Selected actionable gaps before the updated API cleanup: - `base/src/main/java/io/spine/code/fs/AbstractDirectory.java` — constructor lines `40-41`, covered by instantiating a concrete test subclass. - `base/src/main/kotlin/io/spine/io/Files.kt` — lines `57-61`, `70-73`; - branches in `File.toUnix()` and `File.toUnixPath()`. + branches in `File.toUnix()` and `File.toUnixPath()`. The deprecated + `File.toUnix()` API was removed, so only `File.toUnixPath()` remains for + future coverage. - `base/src/main/kotlin/io/spine/io/Paths.kt` — lines `66-70`; branches in `Path.toUnix()`. @@ -98,6 +100,35 @@ Non-actionable note: the surrounding `IOException` paths are exercised; this is documented in the `raise-coverage` coverage-signals reference. +## Deprecated API Removal + +Removed from production sources: + +- `io.spine.util.MoreCollections` deprecated aliases for + `Iterable.theOnly()` and `Iterable.interlaced(...)`. +- `Indent.DEFAULT_SIZE`; `DEFAULT_JAVA_INDENT_SIZE` remains. +- `File.toUnix()`; `File.toUnixPath()` remains. +- `Any.unpackGuessingType()`; `Any.unpackKnownType()` remains. +- `Identifier.findField(...)`; `Field.findIdField(...)` remains. +- `Durations2.ZERO`, `Durations2.isPositive(...)`, and + `Durations2.isNegative(...)`; Protobuf `Durations` replacements remain. +- `FsObject.directory()`; `FsObject.parent()` remains. +- `TypeUrl.toTypeName()`; `TypeUrl.typeName()` remains. +- `SourceFile.isRejections()`. +- Deprecated `CollectionsConverter` and its dedicated tests. +- Deprecated `Text` and its dedicated tests. + +For `Columns`, the mutator overrides are required by `List`, so they were not +removed. Their local deprecation markers and `@deprecated` Javadocs were +removed; the methods still throw `UnsupportedOperationException` and remain +annotated with `@DoNotCall`. + +Verification after removal: + +- `rg -n "@Deprecated|Deprecated\\(|@deprecated" base/src/main base/src/test` + returned no matches. +- `./gradlew :base:build --quiet` passed. + ## Proposed Cases These cases are provisional until the `io.spine.code.fs` usage analysis is @@ -141,10 +172,6 @@ Add `base/src/test/kotlin/io/spine/code/fs/FsObjectSpec.kt`: Extend `base/src/test/kotlin/io/spine/io/FilesSpec.kt`: -- `File.toUnix()` converts Windows-style separators and returns the same - instance for Unix-style paths. - Closes: `Files.kt` lines `57-61`. - - `File.toUnixPath()` converts Windows-style separators and returns the original path string otherwise. Closes: `Files.kt` lines `70-73`. @@ -173,3 +200,7 @@ deprecated API. - 2026-06-01 12:34 WEST — updated scope per user request: remove deprecated `:base` API, analyze `io.spine.code.fs` usage across SpineEventEngine projects, deprecate unused types, and cover only the non-deprecated API. +- 2026-06-01 12:58 WEST — removed deprecated `:base` APIs, deleted dedicated + tests for removed deprecated types, refreshed copyright headers on modified + source files, confirmed no deprecated markers remain under `base/src`, and + passed `./gradlew :base:build --quiet`. diff --git a/base/src/main/java/io/spine/base/Identifier.java b/base/src/main/java/io/spine/base/Identifier.java index 7c6db84668..13ecd106f9 100644 --- a/base/src/main/java/io/spine/base/Identifier.java +++ b/base/src/main/java/io/spine/base/Identifier.java @@ -1,11 +1,11 @@ /* - * Copyright 2022, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -26,10 +26,7 @@ package io.spine.base; -import com.google.errorprone.annotations.InlineMe; import com.google.protobuf.Any; -import com.google.protobuf.Descriptors.Descriptor; -import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Int32Value; import com.google.protobuf.Int64Value; import com.google.protobuf.Message; @@ -39,7 +36,6 @@ import io.spine.string.StringifierRegistry; import org.jspecify.annotations.Nullable; -import java.util.Optional; import java.util.UUID; import static com.google.common.base.Preconditions.checkNotNull; @@ -366,26 +362,6 @@ private Any pack() { return result; } - /** - * Finds the first ID field of the specified type in the given message type. - * - * @deprecated Use {@link Field#findIdField(Class, Descriptor)} instead. - * - * @param idClass - * the class of identifiers - * @param message - * the descriptor of the message type in which to find a field - * @param - * the type of identifiers - * @return the descriptor of the matching field or - * empty {@code Optional} if there is no such a field - */ - @Deprecated - @InlineMe(replacement = "Field.findIdField(idClass, message)", imports = "io.spine.base.Field") - public static Optional findField(Class idClass, Descriptor message) { - return Field.findIdField(idClass, message); - } - @Override @SuppressWarnings("UnnecessaryDefault") // have `default` for future extensibility. public String toString() { diff --git a/base/src/main/java/io/spine/code/fs/FsObject.java b/base/src/main/java/io/spine/code/fs/FsObject.java index b8fd73766d..b55e19075e 100644 --- a/base/src/main/java/io/spine/code/fs/FsObject.java +++ b/base/src/main/java/io/spine/code/fs/FsObject.java @@ -1,11 +1,11 @@ /* - * Copyright 2022, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -26,7 +26,6 @@ package io.spine.code.fs; -import com.google.errorprone.annotations.InlineMe; import org.jspecify.annotations.Nullable; import java.nio.file.Files; @@ -53,17 +52,6 @@ public final Path path() { return path; } - /** - * Obtains the directory to which this object belongs. - * - * @deprecated please use {@link #parent()}. - */ - @Deprecated - @InlineMe(replacement = "this.parent()") - public final @Nullable Path directory() { - return this.parent(); - } - /** * Obtains a parent of this file system object. */ @@ -92,10 +80,9 @@ public boolean equals(Object obj) { if (this == obj) { return true; } - if (!(obj instanceof FsObject)) { + if (!(obj instanceof FsObject other)) { return false; } - var other = (FsObject) obj; return Objects.equals(this.path, other.path); } } diff --git a/base/src/main/java/io/spine/code/proto/SourceFile.java b/base/src/main/java/io/spine/code/proto/SourceFile.java index 9dc5e99b58..1eb2af6650 100644 --- a/base/src/main/java/io/spine/code/proto/SourceFile.java +++ b/base/src/main/java/io/spine/code/proto/SourceFile.java @@ -1,11 +1,11 @@ /* - * Copyright 2022, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -29,9 +29,7 @@ import com.google.common.collect.ImmutableList; import com.google.protobuf.DescriptorProtos.DescriptorProto; import com.google.protobuf.Descriptors.FileDescriptor; -import io.spine.base.RejectionType; import io.spine.code.fs.AbstractSourceFile; -import io.spine.code.java.SimpleClassName; import io.spine.type.MessageType; import java.nio.file.Path; @@ -68,40 +66,6 @@ private static Path toPath(FileDescriptor file) { return result; } - /** - * Returns {@code true} if the source file matches conventions for rejection files. - * - *

A valid rejections file must: - *

    - *
  • be named ending on {@link io.spine.base.MessageFile#REJECTIONS "rejections.proto"}; - *
  • have the {@code java_multiple_files} option set to {@code false}; - *
  • either have a {@code java_outer_classname} value which ends with - * {@linkplain RejectionType#isValidOuterClassName(SimpleClassName)} “Rejections”}, - * or not have the {@code java_outer_classname} option set at all. - *
- * - * @deprecated Source file must not know if it matches a convention for specific types, such as - * rejections. Use {@code RejectionsFile} instead. - */ - @Deprecated - public boolean isRejections() { - // By convention, rejections are generated into one file. - if (descriptor.getOptions() - .getJavaMultipleFiles()) { - return false; - } - var outerClass = SimpleClassName.declaredOuterClassName(descriptor); - - if (outerClass.isEmpty()) { - // There's no outer class name given in options. - // Assuming the file name ends with `rejections.proto`, it's a good rejections file. - return true; - } - - var result = RejectionType.isValidOuterClassName(outerClass.get()); - return result; - } - /** * Obtains descriptor of the file. */ diff --git a/base/src/main/java/io/spine/protobuf/CollectionsConverter.java b/base/src/main/java/io/spine/protobuf/CollectionsConverter.java deleted file mode 100644 index 9d350ba66f..0000000000 --- a/base/src/main/java/io/spine/protobuf/CollectionsConverter.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2022, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.protobuf; - -import com.google.protobuf.Any; -import io.spine.annotation.Internal; -import io.spine.base.ListOfAnys; -import io.spine.base.MapOfAnys; - -import java.util.Map; - -import static com.google.common.base.Preconditions.checkNotNull; - -/** - * Converts instances of {@code List} and {@code Map} into {@link Any}. - * - * @deprecated Use {@link TypeConverter} instead. - */ -@Internal -@Deprecated -public final class CollectionsConverter { - - /** - * Prevents this utility from instantiation. - */ - private CollectionsConverter() { - } - - /** - * Converts the passed items to {@code Any}. - * - *

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

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

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

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

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

Please note that the types of {@code Map}'s keys and values - * should be supported by {@code TypeConverter}. - */ - public static Any toAny(Map map) { - checkNotNull(map); - var asProto = toProto(map); - var result = TypeConverter.toAny(asProto); - return result; - } - - @SuppressWarnings("ResultOfMethodCallIgnored") /* Calling `builder` methods. */ - private static MapOfAnys toProto(Map map) { - var builder = MapOfAnys.newBuilder(); - map.forEach((key, value) -> { - var entry = MapOfAnys.Entry.newBuilder() - .setKey(TypeConverter.toAny(key)) - .setValue(TypeConverter.toAny(value)); - builder.addEntry(entry); - }); - return builder.build(); - } -} diff --git a/base/src/main/java/io/spine/protobuf/Durations2.java b/base/src/main/java/io/spine/protobuf/Durations2.java index 6390fccde6..2d6e9fe67b 100644 --- a/base/src/main/java/io/spine/protobuf/Durations2.java +++ b/base/src/main/java/io/spine/protobuf/Durations2.java @@ -1,11 +1,11 @@ /* - * Copyright 2022, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -23,10 +23,10 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + package io.spine.protobuf; import com.google.common.base.Converter; -import com.google.errorprone.annotations.InlineMe; import com.google.protobuf.Duration; import com.google.protobuf.util.Durations; import io.spine.string.Stringifiers; @@ -60,16 +60,6 @@ @SuppressWarnings("UtilityClass") public final class Durations2 { - /** - * Zero duration. - * - * @deprecated please use {@link Durations#ZERO}. - */ - @Deprecated - public static final Duration ZERO = fromMillis(0L); - - private static final String PROTOBUF_DURATIONS = "com.google.protobuf.util.Durations"; - /** Prevent instantiation of this utility class. */ private Durations2() { } @@ -180,19 +170,6 @@ public static boolean isPositiveOrZero(Duration value) { return result; } - /** - * Returns {@code true} if the passed value is greater than zero, - * {@code false} otherwise. - * - * @deprecated please use {@link Durations#isPositive(Duration)} - */ - @Deprecated - public static boolean isPositive(Duration value) { - checkNotNull(value); - return Durations.isPositive(value); - - } - /** Returns {@code true} if the passed value is zero, {@code false} otherwise. */ public static boolean isZero(Duration value) { checkNotNull(value); @@ -217,17 +194,6 @@ public static boolean isLessThan(Duration value, Duration another) { return result; } - /** - * Returns {@code true} if the passed duration is negative, {@code false} otherwise. - * - * @deprecated please use {@link Durations#isNegative(Duration)}. - */ - @Deprecated - @InlineMe(replacement = "Durations.isNegative(value)", imports = PROTOBUF_DURATIONS) - public static boolean isNegative(Duration value) { - return Durations.isNegative(value); - } - /** * Converts the passed Java Time value. */ diff --git a/base/src/main/java/io/spine/query/Columns.java b/base/src/main/java/io/spine/query/Columns.java index 5b410c7455..3b7067ecb9 100644 --- a/base/src/main/java/io/spine/query/Columns.java +++ b/base/src/main/java/io/spine/query/Columns.java @@ -1,11 +1,11 @@ /* - * Copyright 2022, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -180,10 +180,7 @@ public String toString() { /** * Always throws an {@link UnsupportedOperationException}. - * - * @deprecated as does not apply to this immutable type */ - @Deprecated @Override @DoNotCall("Always throws `UnsupportedOperationException`") public void replaceAll(UnaryOperator> operator) { @@ -192,10 +189,7 @@ public void replaceAll(UnaryOperator> operator) { /** * Always throws an {@link UnsupportedOperationException}. - * - * @deprecated as does not apply to this immutable type */ - @Deprecated @Override @DoNotCall("Always throws `UnsupportedOperationException`") public void sort(Comparator> c) { @@ -204,10 +198,7 @@ public void sort(Comparator> c) { /** * Always throws an {@link UnsupportedOperationException}. - * - * @deprecated as does not apply to this immutable type */ - @Deprecated @Override @DoNotCall("Always throws `UnsupportedOperationException`") public boolean removeIf(Predicate> filter) { @@ -216,10 +207,7 @@ public boolean removeIf(Predicate> filter) { /** * Always throws an {@link UnsupportedOperationException}. - * - * @deprecated as does not apply to this immutable type */ - @Deprecated @Override @DoNotCall("Always throws `UnsupportedOperationException`") public boolean add(RecordColumn column) { @@ -228,10 +216,7 @@ public boolean add(RecordColumn column) { /** * Always throws an {@link UnsupportedOperationException}. - * - * @deprecated as does not apply to this immutable type */ - @Deprecated @Override @DoNotCall("Always throws `UnsupportedOperationException`") public boolean addAll(@NonNull Collection> c) { @@ -240,10 +225,7 @@ public boolean addAll(@NonNull Collection> c) { /** * Always throws an {@link UnsupportedOperationException}. - * - * @deprecated as does not apply to this immutable type */ - @Deprecated @Override @DoNotCall("Always throws `UnsupportedOperationException`") public boolean addAll(int index, @NonNull Collection> c) { @@ -252,10 +234,7 @@ public boolean addAll(int index, @NonNull Collection element) { @@ -264,10 +243,7 @@ public void add(int index, RecordColumn element) { /** * Always throws an {@link UnsupportedOperationException}. - * - * @deprecated as does not apply to this immutable type */ - @Deprecated @Override @DoNotCall("Always throws `UnsupportedOperationException`") public RecordColumn set(int index, RecordColumn element) { @@ -276,10 +252,7 @@ public void add(int index, RecordColumn element) { /** * Always throws an {@link UnsupportedOperationException}. - * - * @deprecated as does not apply to this immutable type */ - @Deprecated @Override @DoNotCall("Always throws `UnsupportedOperationException`") public boolean remove(Object o) { @@ -288,10 +261,7 @@ public boolean remove(Object o) { /** * Always throws an {@link UnsupportedOperationException}. - * - * @deprecated as does not apply to this immutable type */ - @Deprecated @Override @DoNotCall("Always throws `UnsupportedOperationException`") public boolean removeAll(Collection c) { @@ -300,10 +270,7 @@ public boolean removeAll(Collection c) { /** * Always throws an {@link UnsupportedOperationException}. - * - * @deprecated as does not apply to this immutable type */ - @Deprecated @Override @DoNotCall("Always throws `UnsupportedOperationException`") public boolean retainAll(Collection c) { @@ -312,10 +279,7 @@ public boolean retainAll(Collection c) { /** * Always throws an {@link UnsupportedOperationException}. - * - * @deprecated as does not apply to this immutable type */ - @Deprecated @Override @DoNotCall("Always throws `UnsupportedOperationException`") public void clear() { @@ -324,10 +288,7 @@ public void clear() { /** * Always throws an {@link UnsupportedOperationException}. - * - * @deprecated as does not apply to this immutable type */ - @Deprecated @Override @DoNotCall("Always throws `UnsupportedOperationException`") public RecordColumn remove(int index) { diff --git a/base/src/main/java/io/spine/type/TypeUrl.java b/base/src/main/java/io/spine/type/TypeUrl.java index f47ec461ac..6e5538b07c 100644 --- a/base/src/main/java/io/spine/type/TypeUrl.java +++ b/base/src/main/java/io/spine/type/TypeUrl.java @@ -1,5 +1,5 @@ /* - * Copyright 2024, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,6 @@ import com.google.common.base.Splitter; import com.google.errorprone.annotations.Immutable; -import com.google.errorprone.annotations.InlineMe; import com.google.protobuf.Any; import com.google.protobuf.AnyOrBuilder; import com.google.protobuf.Descriptors.Descriptor; @@ -273,17 +272,6 @@ public String toString() { return value(); } - /** - * Converts the instance to {@code TypeName}. - * - * @deprecated Please use {@link #typeName()} and {@code typeName} in Kotlin. - */ - @Deprecated - @InlineMe(replacement = "this.typeName()") - public TypeName toTypeName() { - return typeName(); - } - /** * Obtains the type name component of this type URL. */ diff --git a/base/src/main/java/io/spine/util/Text.java b/base/src/main/java/io/spine/util/Text.java deleted file mode 100644 index 6e93eabf16..0000000000 --- a/base/src/main/java/io/spine/util/Text.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright 2022, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.util; - -import com.google.common.base.Joiner; -import com.google.common.base.Splitter; -import com.google.common.collect.ImmutableList; -import com.google.errorprone.annotations.Immutable; - -import java.util.Iterator; -import java.util.List; -import java.util.Objects; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static io.spine.util.Exceptions.newIllegalArgumentException; -import static java.lang.System.lineSeparator; - -/** - * A text with lines separated by {@link System#lineSeparator()}. - * - * @apiNote Even though this class provides static utilities for splitting and merging - * lines, please prefer using instance methods of this class. - * @deprecated Please use a similar type for - * the Text library. - */ -@Immutable -@Deprecated -public final class Text implements Iterable { - - private static final Splitter SPLITTER = Splitter.on(lineSeparator()); - private static final Joiner JOINER = Joiner.on(lineSeparator()); - - private final ImmutableList lines; - private final String value; - - /** - * Creates a new instance with the given lines. - * - *

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

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

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

This method always returns the same object, so repeated calls have no performance effect. - * - * @see #toString() - */ - public String value() { - return value; - } - - /** - * Tells if the given string is contained by any of the text lines. - * - * @param s - * the string to find. Must not contain a {@linkplain System#lineSeparator() - * line separator}. - * @return {@code true} if at least one line contains the given string, {@code false} otherwise - * @throws IllegalArgumentException - * if the given string contains the {@linkplain System#lineSeparator() - * line separator} - */ - public boolean contains(String s) { - checkArgument(!containsSeparator(s)); - var result = lines.stream().anyMatch(line -> line.contains(s)); - return result; - } - - private static boolean containsSeparator(String s) { - return s.contains(lineSeparator()); - } - - /** - * Obtains the text with joined lines separated by {@linkplain System#lineSeparator() - * line separator}. - */ - @Override - public String toString() { - return value; - } - - @Override - public Iterator iterator() { - return lines.stream().iterator(); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof Text)) { - return false; - } - var strings = (Text) o; - return Objects.equals(value, strings.value); - } - - @Override - public int hashCode() { - return value.hashCode(); - } - - /** - * Splits the passed text separated with {@linkplain System#lineSeparator() - * line separator} into lines. - * - * @return unmodifiable list of code lines - */ - public static List split(String text) { - checkNotNull(text); - return splitter().splitToList(text); - } - - /** - * Join the lines separated with {@linkplain System#lineSeparator() - * line separator}. - */ - public static String join(Iterable lines) { - checkNotNull(lines); - return joiner().join(lines); - } - - /** - * Join the lines separating them with {@linkplain System#lineSeparator() - * line separator}. - */ - public static String join(String[] lines) { - checkNotNull(lines); - return joiner().join(lines); - } - - /** - * Obtains the {@link Joiner} on {@linkplain System#lineSeparator() - * line separator}. - */ - public static Joiner joiner() { - return JOINER; - } - - /** - * Obtains the {@link Splitter} on {@link System#lineSeparator()}. - */ - public static Splitter splitter() { - return SPLITTER; - } -} diff --git a/base/src/main/kotlin/io/spine/io/Files.kt b/base/src/main/kotlin/io/spine/io/Files.kt index 1dbe1f1f30..a642ca0a2e 100644 --- a/base/src/main/kotlin/io/spine/io/Files.kt +++ b/base/src/main/kotlin/io/spine/io/Files.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,22 +44,6 @@ public fun File.replaceExtension(newExtension: String): File { return resolveSibling(nameWithoutExtension + newExt) } -/** - * Has no effect under Windows because file paths are always normalized. - * - * Please use [File.toUnixPath] instead. - */ -@Deprecated( - message = "Please use `toUnixPath()` instead.", - replaceWith = ReplaceWith("toUnixPath()") -) -public fun File.toUnix(): File = - if (path.contains(Separator.Windows)) { - File(path.toUnix()) - } else { - this - } - /** * Obtains the path with [Unix][Separator.Unix] separators. * diff --git a/base/src/main/kotlin/io/spine/protobuf/AnyExts.kt b/base/src/main/kotlin/io/spine/protobuf/AnyExts.kt index e8aef7ae51..061ec811bb 100644 --- a/base/src/main/kotlin/io/spine/protobuf/AnyExts.kt +++ b/base/src/main/kotlin/io/spine/protobuf/AnyExts.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,19 +49,6 @@ public inline fun AnyProto.unpack(): T { return AnyPacker.unpack(this, cls.java) } -/** - * Unpacks this `Any`. - * - * The concrete type of the message is looked up among the known types by - * the value of the `Any.type_url` field. - */ -@Deprecated( - message = "Please use `unpackKnownType()` instead.", - replaceWith = ReplaceWith("unpackKnownType()") -) -public fun AnyProto.unpackGuessingType(): Message = - unpackKnownType() - /** * Unpacks this `Any`. * diff --git a/base/src/main/kotlin/io/spine/string/Indent.kt b/base/src/main/kotlin/io/spine/string/Indent.kt index 9a5795740f..b1368c912b 100644 --- a/base/src/main/kotlin/io/spine/string/Indent.kt +++ b/base/src/main/kotlin/io/spine/string/Indent.kt @@ -1,11 +1,11 @@ /* - * Copyright 2023, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -53,15 +53,6 @@ public data class Indent( public companion object { - /** - * The default indentation, which is primarily used in the generated Java code. - */ - @Deprecated( - message = "Please use `DEFAULT_JAVA_INDENT_SIZE`.", - replaceWith = ReplaceWith("DEFAULT_JAVA_INDENT_SIZE") - ) - public const val DEFAULT_SIZE: Int = 4 - /** * The default size of indentation used in the Java code. */ diff --git a/base/src/main/kotlin/io/spine/util/MoreCollections.kt b/base/src/main/kotlin/io/spine/util/MoreCollections.kt deleted file mode 100644 index 51cab84b8d..0000000000 --- a/base/src/main/kotlin/io/spine/util/MoreCollections.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2025, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.util - -import io.spine.collect.interlaced -import io.spine.collect.theOnly - -/** - * Obtains the only element in the receiver `Iterable`. - * - * @throws NoSuchElementException if the iterable is empty. - * @throws IllegalArgumentException if the iterable contains multiple elements. - */ -@Deprecated( - message = "Please use `io.spine.collect.theOnly()` instead.", - replaceWith = ReplaceWith(imports = ["io.spine.collect.theOnly"], expression = "theOnly()") -) -public fun Iterable.theOnly(): E = theOnly() - -/** - * Builds a `Sequence` which consists of the elements of this `Iterable` and - * the given [infix] between them. - * - * Example: - * - `listOf(0, 1, 2).interlaced(42)` -> `[0, 42, 1, 42, 2]`; - * - `listOf("sea", "Moon", "Earth", "Sun").interlaced("of")` -> - * `["sea", "of", "Moon", "of", "Earth", "of", "Sun"]`; - * - `listOf().interlaced("")` -> `[]`. - */ -@Deprecated( - message = "Please use `io.spine.collect.interlaced()` instead.", - replaceWith = ReplaceWith( - imports = ["io.spine.collect.interlaced"], - expression = "interlaced()" - ) -) -public fun Iterable.interlaced(infix: T): Sequence = interlaced(infix) diff --git a/base/src/test/java/io/spine/protobuf/CollectionsConverterTest.java b/base/src/test/java/io/spine/protobuf/CollectionsConverterTest.java deleted file mode 100644 index d186298aba..0000000000 --- a/base/src/test/java/io/spine/protobuf/CollectionsConverterTest.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2025, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.protobuf; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import io.spine.base.ListOfAnys; -import io.spine.base.MapOfAnys; -import io.spine.testing.UtilityClassTest; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; -import static io.spine.protobuf.TypeConverter.toObject; - -@SuppressWarnings("deprecation") // We still tell until `CollectionsConverter` is removed. -@DisplayName("`CollectionsConverter` should") -class CollectionsConverterTest extends UtilityClassTest { - - CollectionsConverterTest() { - super(CollectionsConverter.class); - } - - @Test - @DisplayName("convert `Iterable`s to `Any`") - void convertLists() { - var source = ImmutableList.of(2, 12, 85, 6); - var result = CollectionsConverter.toAny(source); - assertThat(result) - .isNotNull(); - var unpacked = AnyPacker.unpack(result); - assertThat(unpacked) - .isInstanceOf(ListOfAnys.class); - var listOfAnys = (ListOfAnys) unpacked; - var values = listOfAnys.getValueList(); - for (var index = 0; index < values.size(); index++) { - var element = values.get(index); - var unpackedElement = toObject(element, Integer.class); - assertThat(unpackedElement) - .isEqualTo(source.get(index)); - } - } - - @Test - @DisplayName("convert `Map`s to `Any`") - void convertMaps() { - var source = ImmutableMap.of("first", 1, - "second", 2, - "third", 3); - var result = CollectionsConverter.toAny(source); - assertThat(result) - .isNotNull(); - var unpacked = AnyPacker.unpack(result); - assertThat(unpacked) - .isInstanceOf(MapOfAnys.class); - var mapOfAnys = (MapOfAnys) unpacked; - var entries = mapOfAnys.getEntryList(); - assertThat(entries.size()) - .isEqualTo(source.size()); - for (var entry : entries) { - var packedKey = entry.getKey(); - var key = toObject(packedKey, String.class); - var packedValue = entry.getValue(); - var value = toObject(packedValue, Integer.class); - assertThat(source.keySet()) - .contains(key); - assertThat(value) - .isEqualTo(source.get(key)); - } - } -} diff --git a/base/src/test/kotlin/io/spine/util/TextTest.kt b/base/src/test/kotlin/io/spine/util/TextTest.kt deleted file mode 100644 index 0d0ba9a404..0000000000 --- a/base/src/test/kotlin/io/spine/util/TextTest.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2022, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package io.spine.util - -import com.google.common.testing.NullPointerTester -import com.google.common.truth.Truth.assertThat -import io.kotest.matchers.shouldBe -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows - -@Suppress("DEPRECATION") -internal class TextTest { - - private val nl = System.lineSeparator() - - @Test - fun `handle nulls passed to static methods`() { - NullPointerTester().testAllPublicStaticMethods(Text::class.java) - } - - @Test - fun `split text into lines`() { - val str = "uno${nl}dos${nl}tres" - val text = Text(str) - assertThat(text.lines()).containsExactly("uno", "dos", "tres") - } - - @Test - fun `join 'Iterable'`() { - val iterable = listOf("bir", "iki", "üç") - val text = Text(iterable) - text.toString() shouldBe "bir${nl}iki${nl}üç" - } - - @Test - fun `join an array`() { - val array = arrayOf("one", "two", "three") - val text = Text(array) - text.toString() shouldBe "one${nl}two${nl}three" - } - - @Test - fun `find substring`() { - val text = Text.of("abra", "ka", "dabra") - - assertThrows { text.contains("abra${nl}ka") } - - text.contains("abra") shouldBe true - text.contains("kada") shouldBe false - } - - @Test - fun `must not accept lines with separators`() { - assertThrows { Text.of("un", "${nl}o") } - assertThrows { Text(listOf("dos", "tres${nl}")) } - } - - @Test - fun `always return the same value`() { - val text = Text.of("donna", "be", "la", "mare") - - val value = text.value() - assertThat(value).isSameInstanceAs(text.value()) - } -} From 691dd4706b51f9c30779caaba522672a1a18bbe2 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Mon, 1 Jun 2026 13:35:52 +0100 Subject: [PATCH 12/45] Address `PMD.SimplifyBooleanReturns` --- base/src/main/java/io/spine/code/fs/FsObject.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/base/src/main/java/io/spine/code/fs/FsObject.java b/base/src/main/java/io/spine/code/fs/FsObject.java index b55e19075e..a609ce6b38 100644 --- a/base/src/main/java/io/spine/code/fs/FsObject.java +++ b/base/src/main/java/io/spine/code/fs/FsObject.java @@ -80,9 +80,7 @@ public boolean equals(Object obj) { if (this == obj) { return true; } - if (!(obj instanceof FsObject other)) { - return false; - } - return Objects.equals(this.path, other.path); + return (obj instanceof FsObject other) && + Objects.equals(this.path, other.path); } } From ead6fb8a6e722d68727abb86de5b80119fe1b27e Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Mon, 1 Jun 2026 13:43:34 +0100 Subject: [PATCH 13/45] Restore equality check for `FsObject` --- base/src/main/java/io/spine/code/fs/FsObject.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/base/src/main/java/io/spine/code/fs/FsObject.java b/base/src/main/java/io/spine/code/fs/FsObject.java index a609ce6b38..6d6a4b0818 100644 --- a/base/src/main/java/io/spine/code/fs/FsObject.java +++ b/base/src/main/java/io/spine/code/fs/FsObject.java @@ -80,7 +80,9 @@ public boolean equals(Object obj) { if (this == obj) { return true; } - return (obj instanceof FsObject other) && - Objects.equals(this.path, other.path); - } + if (!(obj instanceof FsObject)) { + return false; + } + var other = (FsObject) obj; + return Objects.equals(this.path, other.path); } } From 1779d2b8c212242aa70199449ea9abe0d38f9bdb Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Mon, 1 Jun 2026 13:47:20 +0100 Subject: [PATCH 14/45] Update coverage test doc --- .agents/tasks/raise-base-coverage.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.agents/tasks/raise-base-coverage.md b/.agents/tasks/raise-base-coverage.md index 53b86813e9..6e76be9605 100644 --- a/.agents/tasks/raise-base-coverage.md +++ b/.agents/tasks/raise-base-coverage.md @@ -37,7 +37,9 @@ gaps closed without weakening Codecov settings. - [x] Document proposed concrete test cases for approval. - [x] Remove deprecated API in the `:base` module. - [ ] Analyze whether `io.spine.code.fs` is used in SpineEventEngine projects. +- [ ] Mark `io.spine.code.fs` types that are not used by any project as deprecated. - [ ] Deprecate `io.spine.code.fs` types that are not used. +- [ ] Analyse whether `RejectionType` is used in Spine SDK projects and deprecate if not. - [ ] Finalize test cases for the remaining non-deprecated API and wait for approval to write tests. - [ ] Add approved Kotlin `*Spec` tests using stubs, not mocks. From 5ba4933d4b75edfb7c1a062806691c4b428abc08 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 1 Jun 2026 21:37:50 +0100 Subject: [PATCH 15/45] Fail when CodeCov upload fails --- .github/workflows/build-on-ubuntu.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-on-ubuntu.yml b/.github/workflows/build-on-ubuntu.yml index cd6b93714c..d109aeae89 100644 --- a/.github/workflows/build-on-ubuntu.yml +++ b/.github/workflows/build-on-ubuntu.yml @@ -34,5 +34,5 @@ jobs: uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: false + fail_ci_if_error: true verbose: true From e08ae463a1518d04a8e9745868be2e2c56145196 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 1 Jun 2026 21:38:04 +0100 Subject: [PATCH 16/45] Update Claude permissions --- .claude/settings.local.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 602e6ceaaa..999d569582 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -4,7 +4,8 @@ "Skill(pre-pr)", "Skill(pre-pr:*)", "Bash(.agents/skills/version-bumped/scripts/version-bumped.sh)", - "Bash(echo \"exit=$?\")" + "Bash(echo \"exit=$?\")", + "Bash(gh pr *)" ] } } From 5363e4c86cec8b9947292add44a6b755b35721b9 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 2 Jun 2026 19:41:15 +0100 Subject: [PATCH 17/45] Pull `config` --- .agents/_TOC.md | 25 -- .agents/advanced-safety-rules.md | 6 - .agents/coding-guidelines.md | 41 -- .agents/common-tasks.md | 6 - .agents/documentation-guidelines.md | 40 -- .agents/documentation-tasks.md | 20 - .agents/guidelines | 1 + .agents/jvm-project.md | 37 -- .agents/project-structure-expectations.md | 21 - .agents/project.md | 39 +- .agents/project.template.md | 18 - .agents/quick-reference-card.md | 7 - .agents/refactoring-guidelines.md | 3 - .agents/running-builds.md | 18 - .agents/safety-rules.md | 49 --- .agents/scripts | 1 + .agents/scripts/api-discovery/.gitignore | 3 - .agents/scripts/api-discovery/README.md | 158 ------- .agents/scripts/api-discovery/clean-cache | 143 ------- .agents/scripts/api-discovery/discover | 153 ------- .agents/scripts/api-discovery/extract-sources | 118 ------ .agents/scripts/api-discovery/lib/common.sh | 364 ---------------- .agents/scripts/api-discovery/update-sibling | 159 ------- .agents/scripts/pre-pr-gate.sh | 82 ---- .agents/scripts/protect-version-file.sh | 55 --- .agents/scripts/publish-version-gate.sh | 95 ----- .agents/scripts/sanitize-source-code.sh | 49 --- .agents/scripts/update-copyright.sh | 48 --- .agents/shared | 1 + .agents/skills | 1 + .agents/skills/api-discovery/SKILL.md | 288 ------------- .../skills/api-discovery/agents/openai.yaml | 4 - .agents/skills/bump-gradle/SKILL.md | 148 ------- .agents/skills/bump-gradle/agents/openai.yaml | 4 - .agents/skills/bump-version/SKILL.md | 143 ------- .../skills/bump-version/agents/openai.yaml | 4 - .agents/skills/check-links/SKILL.md | 331 --------------- .agents/skills/check-links/agents/openai.yaml | 4 - .agents/skills/dependency-audit/SKILL.md | 146 ------- .../dependency-audit/agents/openai.yaml | 4 - .agents/skills/dependency-update/SKILL.md | 283 ------------- .../dependency-update/agents/openai.yaml | 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 | 59 --- .../skills/java-to-kotlin/agents/openai.yaml | 4 - .agents/skills/kotlin-review/SKILL.md | 71 ---- .../skills/kotlin-review/agents/openai.yaml | 4 - .agents/skills/move-files/SKILL.md | 57 --- .agents/skills/move-files/agents/openai.yaml | 4 - .agents/skills/pre-pr/SKILL.md | 203 --------- .agents/skills/pre-pr/agents/openai.yaml | 4 - .agents/skills/raise-coverage/SKILL.md | 251 ----------- .../skills/raise-coverage/agents/openai.yaml | 4 - .../references/coverage-signals.md | 181 -------- .../references/migrate-to-kover.md | 352 ---------------- .agents/skills/review-docs/SKILL.md | 149 ------- .agents/skills/review-docs/agents/openai.yaml | 4 - .../skills/source-command-run-build/SKILL.md | 18 - .agents/skills/update-copyright/SKILL.md | 24 -- .../update-copyright/agents/openai.yaml | 4 - .../scripts/update_copyright.py | 389 ------------------ .../tests/test_update_copyright.py | 130 ------ .agents/skills/version-bumped/SKILL.md | 99 ----- .../skills/version-bumped/agents/openai.yaml | 4 - .../version-bumped/scripts/version-bumped.sh | 276 ------------- .agents/skills/writer/SKILL.md | 85 ---- .agents/skills/writer/agents/openai.yaml | 5 - .../writer/assets/templates/doc-page.md | 23 -- .../writer/assets/templates/kdoc-example.md | 11 - .../assets/templates/kotlin-java-example.md | 13 - .agents/testing.md | 8 - .agents/version-policy.md | 19 - .agents/widow-runt-orphan.jpg | Bin 54071 -> 0 bytes .claude/agents | 1 + .claude/agents/dependency-audit.md | 19 - .claude/agents/kotlin-review.md | 17 - .claude/agents/review-docs.md | 18 - .claude/commands | 1 + .claude/commands/bump-gradle.md | 13 - .claude/commands/bump-version.md | 13 - .claude/commands/dependency-update.md | 16 - .claude/commands/java-to-kotlin.md | 13 - .claude/commands/move-files.md | 12 - .claude/commands/pre-pr.md | 32 -- .claude/commands/raise-coverage.md | 26 -- .claude/commands/review-docs.md | 21 - .claude/commands/run-build.md | 12 - .claude/commands/update-copyright.md | 12 - .claude/commands/write-docs.md | 14 - .claude/settings.local.json | 3 +- .github/copilot-instructions.md | 4 +- .gitmodules | 6 + .junie/guidelines.md | 2 +- AGENTS.md | 21 +- .../gradle/report/coverage/KoverConfig.kt | 7 +- .../main/kotlin/uber-jar-module.gradle.kts | 7 +- config | 2 +- docs/project.md | 38 ++ 102 files changed, 79 insertions(+), 6328 deletions(-) delete mode 100644 .agents/_TOC.md delete mode 100644 .agents/advanced-safety-rules.md delete mode 100644 .agents/coding-guidelines.md delete mode 100644 .agents/common-tasks.md delete mode 100644 .agents/documentation-guidelines.md delete mode 100644 .agents/documentation-tasks.md create mode 120000 .agents/guidelines delete mode 100644 .agents/jvm-project.md delete mode 100644 .agents/project-structure-expectations.md mode change 100644 => 120000 .agents/project.md delete mode 100644 .agents/project.template.md delete mode 100644 .agents/quick-reference-card.md delete mode 100644 .agents/refactoring-guidelines.md delete mode 100644 .agents/running-builds.md delete mode 100644 .agents/safety-rules.md create mode 120000 .agents/scripts delete mode 100644 .agents/scripts/api-discovery/.gitignore delete mode 100644 .agents/scripts/api-discovery/README.md delete mode 100755 .agents/scripts/api-discovery/clean-cache delete mode 100755 .agents/scripts/api-discovery/discover delete mode 100755 .agents/scripts/api-discovery/extract-sources delete mode 100644 .agents/scripts/api-discovery/lib/common.sh delete mode 100755 .agents/scripts/api-discovery/update-sibling delete mode 100755 .agents/scripts/pre-pr-gate.sh delete mode 100755 .agents/scripts/protect-version-file.sh delete mode 100755 .agents/scripts/publish-version-gate.sh delete mode 100755 .agents/scripts/sanitize-source-code.sh delete mode 100755 .agents/scripts/update-copyright.sh create mode 160000 .agents/shared create mode 120000 .agents/skills delete mode 100644 .agents/skills/api-discovery/SKILL.md delete mode 100644 .agents/skills/api-discovery/agents/openai.yaml delete mode 100644 .agents/skills/bump-gradle/SKILL.md delete mode 100644 .agents/skills/bump-gradle/agents/openai.yaml delete mode 100644 .agents/skills/bump-version/SKILL.md delete mode 100644 .agents/skills/bump-version/agents/openai.yaml delete mode 100644 .agents/skills/check-links/SKILL.md delete mode 100644 .agents/skills/check-links/agents/openai.yaml delete mode 100644 .agents/skills/dependency-audit/SKILL.md delete mode 100644 .agents/skills/dependency-audit/agents/openai.yaml delete mode 100644 .agents/skills/dependency-update/SKILL.md delete mode 100644 .agents/skills/dependency-update/agents/openai.yaml delete mode 100644 .agents/skills/gradle-review/SKILL.md delete mode 100644 .agents/skills/gradle-review/agents/openai.yaml delete mode 100644 .agents/skills/gradle-review/practices/README.md delete mode 100644 .agents/skills/gradle-review/practices/tasks.md delete mode 100644 .agents/skills/gradle-review/spine-task-conventions.md delete mode 100644 .agents/skills/java-to-kotlin/SKILL.md delete mode 100644 .agents/skills/java-to-kotlin/agents/openai.yaml delete mode 100644 .agents/skills/kotlin-review/SKILL.md delete mode 100644 .agents/skills/kotlin-review/agents/openai.yaml delete mode 100644 .agents/skills/move-files/SKILL.md delete mode 100644 .agents/skills/move-files/agents/openai.yaml delete mode 100644 .agents/skills/pre-pr/SKILL.md delete mode 100644 .agents/skills/pre-pr/agents/openai.yaml delete mode 100644 .agents/skills/raise-coverage/SKILL.md delete mode 100644 .agents/skills/raise-coverage/agents/openai.yaml delete mode 100644 .agents/skills/raise-coverage/references/coverage-signals.md delete mode 100644 .agents/skills/raise-coverage/references/migrate-to-kover.md delete mode 100644 .agents/skills/review-docs/SKILL.md delete mode 100644 .agents/skills/review-docs/agents/openai.yaml delete mode 100644 .agents/skills/source-command-run-build/SKILL.md delete mode 100644 .agents/skills/update-copyright/SKILL.md delete mode 100644 .agents/skills/update-copyright/agents/openai.yaml delete mode 100755 .agents/skills/update-copyright/scripts/update_copyright.py delete mode 100644 .agents/skills/update-copyright/tests/test_update_copyright.py delete mode 100644 .agents/skills/version-bumped/SKILL.md delete mode 100644 .agents/skills/version-bumped/agents/openai.yaml delete mode 100755 .agents/skills/version-bumped/scripts/version-bumped.sh delete mode 100644 .agents/skills/writer/SKILL.md delete mode 100644 .agents/skills/writer/agents/openai.yaml delete mode 100644 .agents/skills/writer/assets/templates/doc-page.md delete mode 100644 .agents/skills/writer/assets/templates/kdoc-example.md delete mode 100644 .agents/skills/writer/assets/templates/kotlin-java-example.md delete mode 100644 .agents/testing.md delete mode 100644 .agents/version-policy.md delete mode 100644 .agents/widow-runt-orphan.jpg create mode 120000 .claude/agents delete mode 100644 .claude/agents/dependency-audit.md delete mode 100644 .claude/agents/kotlin-review.md delete mode 100644 .claude/agents/review-docs.md create mode 120000 .claude/commands delete mode 100644 .claude/commands/bump-gradle.md delete mode 100644 .claude/commands/bump-version.md delete mode 100644 .claude/commands/dependency-update.md delete mode 100644 .claude/commands/java-to-kotlin.md delete mode 100644 .claude/commands/move-files.md delete mode 100644 .claude/commands/pre-pr.md delete mode 100644 .claude/commands/raise-coverage.md delete mode 100644 .claude/commands/review-docs.md delete mode 100644 .claude/commands/run-build.md delete mode 100644 .claude/commands/update-copyright.md delete mode 100644 .claude/commands/write-docs.md create mode 100644 docs/project.md diff --git a/.agents/_TOC.md b/.agents/_TOC.md deleted file mode 100644 index 2f9ba238b2..0000000000 --- a/.agents/_TOC.md +++ /dev/null @@ -1,25 +0,0 @@ -# Table of Contents - -1. [Quick Reference Card](quick-reference-card.md) -2. [JVM project requirements](jvm-project.md) — language, build, and review checklist shared by all JVM repos -3. [Coding guidelines](coding-guidelines.md) -4. [Documentation & comments](documentation-guidelines.md) -5. [Documentation tasks](documentation-tasks.md) -6. [Running builds](running-builds.md) -7. [Version policy](version-policy.md) -8. [Project structure expectations](project-structure-expectations.md) -9. [Testing](testing.md) -10. [Safety rules](safety-rules.md) -11. [Advanced safety rules](advanced-safety-rules.md) -12. [Refactoring guidelines](refactoring-guidelines.md) -13. [Common tasks](common-tasks.md) -14. [Team memory](memory/MEMORY.md) -15. [Task plans](tasks/README.md) -16. [Java to Kotlin conversion](skills/java-to-kotlin/SKILL.md) -17. [Dependency update](skills/dependency-update/SKILL.md) -18. [Documentation review](skills/review-docs/SKILL.md) -19. [Pre-PR checklist](skills/pre-pr/SKILL.md) -20. [Kotlin code review](skills/kotlin-review/SKILL.md) -21. [Dependency audit](skills/dependency-audit/SKILL.md) -22. [Gradle review](skills/gradle-review/SKILL.md) -23. [Raise test coverage](skills/raise-coverage/SKILL.md) diff --git a/.agents/advanced-safety-rules.md b/.agents/advanced-safety-rules.md deleted file mode 100644 index e4105813fc..0000000000 --- a/.agents/advanced-safety-rules.md +++ /dev/null @@ -1,6 +0,0 @@ -# 🚨 Advanced safety rules - -- Do **not** auto-update external dependencies without explicit request. -- Do **not** inject analytics or telemetry code. -- Flag any usage of unsafe constructs (e.g., reflection, I/O on the main thread). -- Avoid generating blocking calls inside coroutines. diff --git a/.agents/coding-guidelines.md b/.agents/coding-guidelines.md deleted file mode 100644 index 8c0a60f34a..0000000000 --- a/.agents/coding-guidelines.md +++ /dev/null @@ -1,41 +0,0 @@ -# 🧾 Coding guidelines - -## Core principles - -- Adhere to [Spine Event Engine Documentation][spine-docs] for coding style. -- Generate code that compiles cleanly and passes static analysis. -- Respect existing architecture, naming conventions, and project structure. -- Write clear, incremental commits with descriptive messages. -- Include automated tests for any code change that alters functionality. - -## Kotlin best practices - -### ✅ Prefer -- **Kotlin idioms** over Java-style approaches: - - Extension functions - - `when` expressions - - Smart casts - - Data classes and sealed classes - - Immutable data structures -- **Simple nouns** over composite nouns (`user` > `userAccount`) -- **Generic parameters** over explicit variable types (`val list = mutableList()`) -- **Java interop annotations** only when needed (`@file:JvmName`, `@JvmStatic`) -- **Kotlin DSL** for Gradle files -- **Kotlin Protobuf DSL** (`myMessage { field = value }`) over Java builder chains - -### ❌ Avoid -- Mutable data structures -- Java-style verbosity (builders with setters) -- Java Protobuf builders in Kotlin code (`newBuilder()`, `toBuilder()`) unless interop requires them -- Redundant null checks (`?.let` misuse) -- Using `!!` unless clearly justified -- Type names in variable names (`userObject`, `itemList`) -- String duplication (use constants in companion objects) -- Mixing Groovy and Kotlin DSLs in build logic -- Reflection unless specifically requested - -## Text formatting - - ✅ Replace double empty lines with a single empty line in the code. - - ✅ Remove trailing space characters in the code. - -[spine-docs]: https://github.com/SpineEventEngine/documentation/wiki diff --git a/.agents/common-tasks.md b/.agents/common-tasks.md deleted file mode 100644 index 5ee954d835..0000000000 --- a/.agents/common-tasks.md +++ /dev/null @@ -1,6 +0,0 @@ -# 📋 Common tasks - -- **Adding a new dependency**: Update relevant files in `buildSrc` directory. -- **Creating a new module**: Follow existing module structure patterns. -- **Documentation**: Use KDoc style for public and internal APIs. -- **Testing**: Create comprehensive tests using Kotest assertions. diff --git a/.agents/documentation-guidelines.md b/.agents/documentation-guidelines.md deleted file mode 100644 index e034501e46..0000000000 --- a/.agents/documentation-guidelines.md +++ /dev/null @@ -1,40 +0,0 @@ -# Documentation & comments - -## Commenting guidelines -- Avoid inline comments in production code unless necessary. -- Inline comments are helpful in tests. -- When using TODO comments, follow the format on the [dedicated page][todo-comments]. -- File and directory names should be formatted as code. - -## 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 (`//`). -- Single-paragraph headers do not require the trailing empty comment line. - -## Avoid widows, runts, orphans, or rivers - -Agents should **AVOID** text flow patters illustrated -on [this diagram](widow-runt-orphan.jpg). - -[todo-comments]: https://github.com/SpineEventEngine/documentation/wiki/TODO-comments diff --git a/.agents/documentation-tasks.md b/.agents/documentation-tasks.md deleted file mode 100644 index 8ac4660dbe..0000000000 --- a/.agents/documentation-tasks.md +++ /dev/null @@ -1,20 +0,0 @@ -# 📄 Documentation tasks - -1. Ensure all public and internal APIs have KDoc examples. -2. Add in-line code blocks for clarity in tests. -3. Convert inline API comments in Java to KDoc in Kotlin: - ```java - // Literal string to be inlined whenever a placeholder references a non-existent argument. - private final String missingArgumentMessage = "[MISSING ARGUMENT]"; - ``` - transforms to: - ```kotlin - /** - * Literal string to be inlined whenever a placeholder references a non-existent argument. - */ - private val missingArgumentMessage = "[MISSING ARGUMENT]" - ``` - -4. Javadoc -> KDoc conversion tasks: - - Remove `

` tags in the line with text: `"

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

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

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

` removal, etc.). -- `.agents/skills/writer/SKILL.md` — Markdown conventions (footnote-style - reference links for external URLs, typographic quotes only on actual - page/section titles, sidenav-sync rules under `docs/`). -- `.agents/running-builds.md` — for doc-only Kotlin/Java changes the right - build is `./gradlew dokka` (no tests required). - -## Review procedure - -1. **Scope the diff.** Obtain the change set via `git diff --staged` or - `git diff ...HEAD` depending on what the user describes. Restrict - to files matching: - - `**/*.kt`, `**/*.kts`, `**/*.java` (for KDoc/Javadoc inside sources) - - `**/*.proto` (for file-level documentation headers) - - `**/*.md` (Markdown docs) - Do **not** review the full repo — only what changed. - 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 - placement, and KDoc completeness needs the whole paragraph and the - surrounding declarations. - -3. **Stay in scope.** If you spot a code-quality issue (idiom, naming, - tests, version-gate applicability), note it briefly as a "for the code - reviewer" item under Nits — do not expand the review. - -## Checks - -### A. KDoc / Javadoc inside sources - -- **Public and internal APIs carry KDoc.** Per `documentation-tasks.md`, - KDoc should include at least one usage example for non-trivial APIs. - Missing KDoc on a new or modified public/internal symbol is a Should-fix. -- **No Javadoc residue in Kotlin.** When converting from Java: - - `

` tags on a text line removed (`"

This"` → `"This"`). - - `

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

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

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

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

` and preserve meaning. - - Prefer short paragraphs and blank lines over HTML formatting. - -## Validate changes - -- For code changes, follow `.agents/running-builds.md`. -- For documentation-only changes in Kotlin/Java sources, prefer `./gradlew dokka`. diff --git a/.agents/skills/writer/agents/openai.yaml b/.agents/skills/writer/agents/openai.yaml deleted file mode 100644 index 44eaa4e241..0000000000 --- a/.agents/skills/writer/agents/openai.yaml +++ /dev/null @@ -1,5 +0,0 @@ -interface: - display_name: "Writer" - short_description: "Write and update user/developer docs" - default_prompt: "Write or revise documentation in this repository (for example: README.md, docs/**, CONTRIBUTING.md, and API documentation/KDoc). Follow local documentation guidelines in .agents/*.md, keep changes concise and actionable, and include concrete examples and commands where appropriate." - diff --git a/.agents/skills/writer/assets/templates/doc-page.md b/.agents/skills/writer/assets/templates/doc-page.md deleted file mode 100644 index f405b71e15..0000000000 --- a/.agents/skills/writer/assets/templates/doc-page.md +++ /dev/null @@ -1,23 +0,0 @@ -# Title - -## Goal - -State what the reader will accomplish. - -## Prerequisites - -- List versions/tools the reader needs. - -## Steps - -1. Do the first thing. -2. Do the next thing. - -## Verify - -Show how the reader can confirm success. - -## Troubleshooting - -- Common failure: likely cause → fix. - diff --git a/.agents/skills/writer/assets/templates/kdoc-example.md b/.agents/skills/writer/assets/templates/kdoc-example.md deleted file mode 100644 index fdbd9b6a0d..0000000000 --- a/.agents/skills/writer/assets/templates/kdoc-example.md +++ /dev/null @@ -1,11 +0,0 @@ -````kotlin -/** - * Explain what this API does in one sentence. - * - * ## Example - * ```kotlin - * // Show the typical usage pattern. - * val result = doThing() - * ``` - */ -```` diff --git a/.agents/skills/writer/assets/templates/kotlin-java-example.md b/.agents/skills/writer/assets/templates/kotlin-java-example.md deleted file mode 100644 index 5517516f56..0000000000 --- a/.agents/skills/writer/assets/templates/kotlin-java-example.md +++ /dev/null @@ -1,13 +0,0 @@ -{{< code-tabs langs="Kotlin, Java">}} - -{{< code-tab lang="Kotlin" >}} -```kotlin -``` -{{< /code-tab >}} - -{{< code-tab lang="Java" >}} -```java -``` -{{< /code-tab >}} - -{{< /code-tabs >}} diff --git a/.agents/testing.md b/.agents/testing.md deleted file mode 100644 index f81bdbf3d3..0000000000 --- a/.agents/testing.md +++ /dev/null @@ -1,8 +0,0 @@ -# 🧪 Testing - -- Do not use mocks, use stubs. -- Prefer [Kotest assertions][kotest-assertions] over assertions from JUnit or Google Truth. -- Generate unit tests for APIs (handles edge cases/scenarios). -- Supply scaffolds for typical Kotlin patterns (`when`, sealed classes). - -[kotest-assertions]: https://kotest.io/docs/assertions/assertions.html diff --git a/.agents/version-policy.md b/.agents/version-policy.md deleted file mode 100644 index 3e8abd5495..0000000000 --- a/.agents/version-policy.md +++ /dev/null @@ -1,19 +0,0 @@ -# Version policy - -When a repository has `version.gradle.kts` at the project root, it follows the -[Spine SDK Versioning policy][wiki-versioning]. The version is kept in that -file and follows [Semantic Versioning 2.0.0][semver] with Spine-specific -extensions (snapshot `NUMBER`, patch, and flavor suffixes). - -For repositories with root `version.gradle.kts`, PRs without a version bump -fail CI. Repositories without that file are not versioned Gradle Build Tools -projects; their version check is not applicable, and agents must not create -`version.gradle.kts` just to satisfy `/pre-pr`. - -For the bump procedure in repositories that have root `version.gradle.kts` — -version-number selection, the commit-message convention, the rebuild, -dependency-report updates, and conflict resolution — use the -[`bump-version`](skills/bump-version/SKILL.md) skill. - -[semver]: https://semver.org/ -[wiki-versioning]: https://github.com/SpineEventEngine/documentation/wiki/Versioning diff --git a/.agents/widow-runt-orphan.jpg b/.agents/widow-runt-orphan.jpg deleted file mode 100644 index 284b02a47d57121b3fa0356a6805428ad2030c8c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54071 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 KDoc. -argument-hint: "" -allowed-tools: Read, Edit, Write, Bash(./gradlew:*), Bash(git status:*), Grep, Glob ---- - -Follow the `java-to-kotlin` skill exactly: - -- Skill: `.agents/skills/java-to-kotlin/SKILL.md` -- Target: $ARGUMENTS -- Preserve behavior. Convert Javadoc to KDoc, `@Nullable` to nullable Kotlin types, getters/setters to properties, static methods to companion objects or top-level functions. -- After each file, run `./gradlew compileKotlin` (or the relevant module's compile task) to verify. -- Honor `.agents/coding-guidelines.md` for Kotlin idioms. diff --git a/.claude/commands/move-files.md b/.claude/commands/move-files.md deleted file mode 100644 index 25885f9d77..0000000000 --- a/.claude/commands/move-files.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -description: Move or rename files/directories, updating all references and build metadata. -argument-hint: " " -allowed-tools: Read, Edit, Bash(git mv:*), Bash(git status:*), Bash(git ls-files:*), Grep, Glob ---- - -Follow the `move-files` skill exactly: - -- Skill: `.agents/skills/move-files/SKILL.md` -- Operation: $ARGUMENTS -- Preflight (run `git status --short`, classify scope) -> Search for all old identifiers -> Move with `git mv` -> Repair references (imports, build metadata, docs) -> Verify. -- Report: Moved[], UpdatedRefs[], Verification[], Risks[]. diff --git a/.claude/commands/pre-pr.md b/.claude/commands/pre-pr.md deleted file mode 100644 index 24499cc517..0000000000 --- a/.claude/commands/pre-pr.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -description: Run the applicable pre-PR checklist (version gate, build/check, reviewers) and write a sentinel so `gh pr create` is unblocked. -argument-hint: "[base-ref]" -allowed-tools: Read, Write, Grep, Glob, Agent, Bash ---- - -Follow the `pre-pr` skill exactly: - -- Skill: `.agents/skills/pre-pr/SKILL.md` -- Base ref: $ARGUMENTS (treat empty as `master`). -- Detect whether the repository-root `version.gradle.kts` exists. If it is - absent at both the base ref and `HEAD`, the version check is `N/A`; do not - create the file and do not ask for `/bump-version`. -- Run the build/check command selected by the skill and - `.agents/running-builds.md`. The command may be Gradle or non-Gradle. -- Dispatch the reviewers as Claude subagents in parallel — send a single - message with multiple Agent tool uses: - - `kotlin-review` when `.kt|.kts|.java` files changed. - - `review-docs` when `.md` files or KDoc inside sources changed. - - `dependency-audit` when any file under - `buildSrc/src/main/kotlin/io/spine/dependency/` changed. -- Pass the version-check status to reviewers. If it is `N/A`, tell them: - "This repository has no root `version.gradle.kts`; a version bump is not - applicable and must not be reported as missing." -- Each reviewer is read-only; do not pass it edit tools. -- On any reviewer returning `REQUEST CHANGES`, treat the overall result - as `FAIL` and stop before writing the sentinel as `PASS`. -- Sentinel location: `$(git rev-parse --show-toplevel)/.git/pre-pr.ok`, - format per the skill (`head=`, `branch=`, `status=`, `timestamp=`, - `build=`, `reviewers=`, `version=`). Use `git rev-parse HEAD` for the - SHA and `date -u +%Y-%m-%dT%H:%M:%SZ` for the timestamp. -- Do NOT run `gh pr create`. That is the user's next step. diff --git a/.claude/commands/raise-coverage.md b/.claude/commands/raise-coverage.md deleted file mode 100644 index c428055ff5..0000000000 --- a/.claude/commands/raise-coverage.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -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/.claude/commands/review-docs.md b/.claude/commands/review-docs.md deleted file mode 100644 index f8043f0ea1..0000000000 --- a/.claude/commands/review-docs.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -description: Review documentation changes (KDoc/Javadoc and Markdown) against Spine documentation conventions. -argument-hint: "[base-ref | --staged | paths...]" -allowed-tools: Read, Grep, Glob, Bash(git diff:*), Bash(git log:*), Bash(git status:*), Bash(git rev-parse:*), Bash(git ls-files:*) ---- - -Follow the `review-docs` skill exactly: - -- Skill: `.agents/skills/review-docs/SKILL.md` -- Scope / flags: $ARGUMENTS - - Empty: review the current branch's diff against `master` (`git diff master...HEAD`). - - `--staged`: review staged changes only (`git diff --staged`). - - A base ref (e.g. `master`, `origin/master`, a commit SHA): review `git diff ...HEAD`. - - Explicit paths: limit the review to those paths in addition to the diff scope. -- The skill owns the procedure, the per-area checks (KDoc/Javadoc, Markdown, - prose flow, terminology), and the output format (Must fix / Should fix / - Nits + one-line verdict). -- Stay in scope: documentation only. If a code-quality issue surfaces, - note it briefly as a Nit pointing at `/review` (or the `kotlin-review` - agent) — do not expand the review. -- Read-only: do not edit files, do not run builds. diff --git a/.claude/commands/run-build.md b/.claude/commands/run-build.md deleted file mode 100644 index 8a8d84ca0b..0000000000 --- a/.claude/commands/run-build.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -description: Build the project the right way based on what changed (proto vs. Kotlin/Java vs. docs). -allowed-tools: Bash(./gradlew:*), Bash(git status:*), Bash(git diff:*) ---- - -Decide which build to run by looking at `git status --short` and `git diff --name-only`: - -- If any `.proto` files changed: `./gradlew clean build` -- Else if Kotlin or Java source changed: `./gradlew build` -- Else if only docs/comments changed (KDoc / Javadoc / Markdown): `./gradlew dokka`. Tests are NOT required for doc-only changes. - -Report the chosen command and its result. See `.agents/running-builds.md`. diff --git a/.claude/commands/update-copyright.md b/.claude/commands/update-copyright.md deleted file mode 100644 index 076fb6133a..0000000000 --- a/.claude/commands/update-copyright.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -description: Refresh copyright headers from the IntelliJ profile, replacing today.year with the current year. -argument-hint: "[paths...]" -allowed-tools: Bash(python3 .agents/skills/update-copyright/scripts/update_copyright.py:*), Read ---- - -Follow the `update-copyright` skill exactly: - -- Skill: `.agents/skills/update-copyright/SKILL.md` -- Run: `python3 .agents/skills/update-copyright/scripts/update_copyright.py $ARGUMENTS` -- If $ARGUMENTS is empty, run once with `--dry-run`, show the output to the user, then run without `--dry-run`. -- Never add a header to a file that doesn't already have one. diff --git a/.claude/commands/write-docs.md b/.claude/commands/write-docs.md deleted file mode 100644 index b9b9a742b2..0000000000 --- a/.claude/commands/write-docs.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -description: Write or update Markdown / KDoc documentation per Spine documentation conventions. -argument-hint: "" -allowed-tools: Read, Edit, Write, Grep, Glob ---- - -Follow the `writer` skill exactly: - -- Skill: `.agents/skills/writer/SKILL.md` -- Topic / target: $ARGUMENTS -- Decide audience first (end user, contributor, maintainer, tooling). -- Prefer updating an existing doc over creating a new one. -- Keep `docs/data/docs/

//sidenav.yml` in sync when adding, removing, moving, or renaming pages under `docs/content/docs/
/`. -- Honor `.agents/documentation-guidelines.md` and `.agents/documentation-tasks.md`. diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 999d569582..602e6ceaaa 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -4,8 +4,7 @@ "Skill(pre-pr)", "Skill(pre-pr:*)", "Bash(.agents/skills/version-bumped/scripts/version-bumped.sh)", - "Bash(echo \"exit=$?\")", - "Bash(gh pr *)" + "Bash(echo \"exit=$?\")" ] } } diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 8a5ab934a9..039657bee2 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -10,8 +10,8 @@ repository root — read it first. If `.agents/project.md` exists, read it before reviewing. It provides the language, architecture, role, and code review checklist for this specific repo. -Additional guidelines are in `.agents/` — see `.agents/_TOC.md` for the index -(if present; Hugo repos do not include this file). +Additional guidelines are in `.agents/guidelines/` — see +`.agents/guidelines/_TOC.md` for the index. ## Do not review diff --git a/.gitmodules b/.gitmodules index 94e8664d69..5b3352bf4e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,9 @@ [submodule "config"] path = config url = https://github.com/SpineEventEngine/config +[submodule ".agents/shared"] + path = .agents/shared + url = https://github.com/SpineEventEngine/agents.git + branch = master + update = checkout + ignore = all diff --git a/.junie/guidelines.md b/.junie/guidelines.md index 5160f499e6..7c1f866547 100644 --- a/.junie/guidelines.md +++ b/.junie/guidelines.md @@ -1,6 +1,6 @@ # Guidelines for Junie and AI Agent from JetBrains -Read the `../.agents/_TOC.md` file to understand: +Read the `../.agents/guidelines/_TOC.md` file to understand: - the agent responsibilities, - project overview, - coding guidelines, diff --git a/AGENTS.md b/AGENTS.md index c2a8da50e9..d96636dd27 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,16 +4,21 @@ If `.agents/project.md` exists in this repository, read it first — it describes the language, architecture, and role of this specific repo within the Spine SDK -organisation. To create one, copy `.agents/project.template.md` (or the -relevant language template) and fill it in. If `project.md` links to a shared -requirements file (e.g. `jvm-project.md`), read that too. +organisation. It is a symlink to `docs/project.md`; to create one, copy +`.agents/guidelines/project.template.md` to `docs/project.md` and fill it in. If it +links to a shared requirements file (e.g. `jvm-project.md`), read that too. -- Start every session by reading `.agents/quick-reference-card.md` (if present). +- Start every session by reading `.agents/guidelines/quick-reference-card.md` (if present). - For specific tasks (code review, PR prep, dependency updates, docs, etc.), prefer the matching skill from `.agents/skills/`. -- Full standards reference: `.agents/_TOC.md` (if present) — consult when a +- Full standards reference: `.agents/guidelines/_TOC.md` (if present) — consult when a skill doesn't cover the needed context. +Shared skills, scripts, and guidelines come from the `.agents/shared` submodule (the +[`agents`][agents-repo] repository) exposed via symlinks. +`./config/pull` initializes and floats it automatically; on a fresh clone that skips +`pull`, run `git submodule update --init --remote .agents/shared`. + ## Commit and history safety **Do not commit, push, tag, rebase, merge, cherry-pick, or otherwise write to git history** @@ -26,7 +31,7 @@ unless one of the following is true *right now*: Authorization does not carry over between turns or sessions. When in doubt: stage changes, show the diff, and stop — let the user commit. -See [`.agents/safety-rules.md`](.agents/safety-rules.md) → *Commits and history-writing*. +See [`.agents/guidelines/safety-rules.md`](.agents/guidelines/safety-rules.md) → *Commits and history-writing*. ## Other safety rules @@ -35,7 +40,7 @@ See [`.agents/safety-rules.md`](.agents/safety-rules.md) → *Commits and histor - No analytics, telemetry, or tracking code. - No reflection or unsafe code without explicit approval. -See [`.agents/safety-rules.md`](.agents/safety-rules.md) for the full list. +See [`.agents/guidelines/safety-rules.md`](.agents/guidelines/safety-rules.md) for the full list. ## Moving files @@ -113,3 +118,5 @@ In consumer repositories, skip without comment any path matching: - `gradle/`, `gradlew`, `gradlew.bat` - `.codecov.yml`, `.gitignore`, `gradle.properties`, `lychee.toml` - `.github/workflows/` — unless the workflow was introduced by this repo + +[agents-repo]: https://github.com/SpineEventEngine/agents diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/KoverConfig.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/KoverConfig.kt index bc1a6d3c64..4f9e7c8fa1 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/KoverConfig.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/KoverConfig.kt @@ -35,7 +35,6 @@ 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.gradle.kotlin.dsl.configure import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet @@ -92,9 +91,9 @@ private const val KOTLIN_FILE_CLASS_SUFFIX: String = "Kt" * - 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 - * [io.spine.gradle.report.coverage.JacocoConfig]. The behaviour mirrors what - * `JacocoConfig.applyTo(rootProject)` provided, but is wired through Kover + * 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") 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/config b/config index c875ea22e1..527f658daa 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit c875ea22e14836bbb71b1ac3252f08355c3a1076 +Subproject commit 527f658daa8d54ff5ab31cdcaf7b6f94afe7b8a0 diff --git a/docs/project.md b/docs/project.md new file mode 100644 index 0000000000..9e415d8a45 --- /dev/null +++ b/docs/project.md @@ -0,0 +1,38 @@ +# Project: base-libraries + +## Overview + +`base-libraries` is a foundational JVM repository in the Spine SDK organisation. +It hosts the common data types, annotations, environment helpers, and +parsing/serialization utilities that the rest of the Spine SDK (notably +[`core-jvm`](https://github.com/SpineEventEngine/core-java)) depends on. The +artifacts published from this repo sit at the bottom of the Spine dependency +graph, so changes here ripple into most other Spine projects. + +## Architecture + +Role: **library** (multi-module Gradle build) publishing the following Maven +artifacts under the `io.spine` group: + +- `annotations` — annotation types used across the Spine SDK. +- `base` — common data types and utilities. Not consumed directly by + end users; re-exposed as an `api` dependency by `spine-client` and + `spine-server` in `core-jvm`. +- `environment` — runtime environment detection helpers. +- `format` — parsers for YAML, JSON, binary Protobuf, and Protobuf JSON; + used internally by Spine SDK components. + +Key constraints: + +- Public API stability matters: downstream Spine repos pin to versions + published from here, so removals and signature changes are breaking. +- No analytics, telemetry, reflection, or unsafe code (see + `.agents/safety-rules.md`). +- Versioning follows the Spine SDK policy (`.agents/version-policy.md`); + CI's `Version Guard` rejects branches that reuse a published version. +- Dependency declarations live under + `buildSrc/src/main/kotlin/io/spine/dependency/` and are audited by the + `dependency-audit` skill. + +Read [`.agents/jvm-project.md`](jvm-project.md) for build stack, coding +style, tests, and versioning. From f50adf5577dd63f21255b32d8626e4f88a043a53 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 2 Jun 2026 20:50:36 +0100 Subject: [PATCH 18/45] Fix file layout --- base/src/test/kotlin/io/spine/code/proto/FieldDeclarationSpec.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/base/src/test/kotlin/io/spine/code/proto/FieldDeclarationSpec.kt b/base/src/test/kotlin/io/spine/code/proto/FieldDeclarationSpec.kt index 938c68878c..848651badd 100644 --- a/base/src/test/kotlin/io/spine/code/proto/FieldDeclarationSpec.kt +++ b/base/src/test/kotlin/io/spine/code/proto/FieldDeclarationSpec.kt @@ -52,6 +52,7 @@ import org.junit.jupiter.api.Test @Suppress("TooManyFunctions") @DisplayName("`FieldDeclaration` should") internal class FieldDeclarationSpec { + @Test fun `not accept 'null's on construction`() { val descriptor = Any.getDescriptor() From 19a9f7a3a745b531e9943dded938da1b81383e50 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 2 Jun 2026 22:33:39 +0100 Subject: [PATCH 19/45] Add tests Also: * Use backticked names instead of `@DisplayName`. --- .agents/memory/MEMORY.md | 1 + .../memory/feedback/kotlin-test-formatting.md | 23 ++++++ .../io/spine/base/RejectionTypeKtSpec.kt | 53 +++++++++++++ .../kotlin/io/spine/base/ThrowableExtsTest.kt | 4 +- .../spine/code/fs/AbstractSourceFileKtSpec.kt | 77 ++++++++++++++++++ .../io/spine/code/proto/FieldContextKtSpec.kt | 74 ++++++++++++++++++ .../spine/code/proto/FieldDeclarationSpec.kt | 22 ++---- .../io/spine/code/proto/FileNameSpec.kt | 11 +-- .../io/spine/code/proto/MessageTypeSpec.kt | 24 +++--- .../io/spine/code/proto/PackageNameSpec.kt | 7 +- .../io/spine/code/proto/TypeSetKtSpec.kt | 62 +++++++++++++++ .../src/test/kotlin/io/spine/io/EnsureSpec.kt | 13 ++-- .../src/test/kotlin/io/spine/io/Files2Spec.kt | 5 +- base/src/test/kotlin/io/spine/io/GlobSpec.kt | 4 +- .../io/spine/protobuf/DescriptorExtsSpec.kt | 5 +- .../kotlin/io/spine/protobuf/DiffKtSpec.kt | 78 +++++++++++++++++++ .../io/spine/protobuf/Durations2Spec.kt | 41 ++++------ .../io/spine/security/InvocationGuardTest.kt | 5 +- .../io/spine/string/StringifiersSpec.kt | 31 ++++---- .../kotlin/io/spine/type/ApiOptionSpec.kt | 10 +-- .../io/spine/type/DescriptorExtsSpec.kt | 5 +- .../io/spine/type/FileDescriptorExtsSpec.kt | 5 +- .../src/test/kotlin/io/spine/type/JsonSpec.kt | 9 ++- .../kotlin/io/spine/type/KnownTypesSpec.kt | 8 +- .../kotlin/io/spine/type/MessageExtsSpec.kt | 5 +- .../kotlin/io/spine/type/ProtoTextExtsSpec.kt | 10 +-- .../spine/type/ServiceDescriptorExtsSpec.kt | 5 +- .../test/kotlin/io/spine/type/TypeUrlSpec.kt | 14 ++-- .../io/spine/util/Preconditions2Test.kt | 17 ++-- docs/dependencies/dependencies.md | 8 +- 30 files changed, 482 insertions(+), 154 deletions(-) create mode 100644 .agents/memory/feedback/kotlin-test-formatting.md create mode 100644 base/src/test/kotlin/io/spine/base/RejectionTypeKtSpec.kt create mode 100644 base/src/test/kotlin/io/spine/code/fs/AbstractSourceFileKtSpec.kt create mode 100644 base/src/test/kotlin/io/spine/code/proto/FieldContextKtSpec.kt create mode 100644 base/src/test/kotlin/io/spine/code/proto/TypeSetKtSpec.kt create mode 100644 base/src/test/kotlin/io/spine/protobuf/DiffKtSpec.kt diff --git a/.agents/memory/MEMORY.md b/.agents/memory/MEMORY.md index 2c8045c6ed..6d811aec84 100644 --- a/.agents/memory/MEMORY.md +++ b/.agents/memory/MEMORY.md @@ -6,6 +6,7 @@ See [README.md](README.md) for the format and routing rules. ## Feedback (validated patterns & corrections) - [copilot-review-request](feedback/copilot-review-request.md) — GraphQL `requestReviews` with `botIds: ["BOT_kgDOCnlnWA"]`; REST endpoint silently no-ops on re-requests. +- [kotlin-test-formatting](feedback/kotlin-test-formatting.md) — `@Nested` should be on the same line as `inner class`, and backticked name on the next line. ## Project (durable context & rationale) diff --git a/.agents/memory/feedback/kotlin-test-formatting.md b/.agents/memory/feedback/kotlin-test-formatting.md new file mode 100644 index 0000000000..cd9a66042b --- /dev/null +++ b/.agents/memory/feedback/kotlin-test-formatting.md @@ -0,0 +1,23 @@ +# Kotlin Test Formatting + +## Backticked inner classes + +When using backticked descriptive names for inner classes in Kotlin tests: +1. The `@Nested` annotation must be on the same line as the `inner class` declaration. +2. The backticked class name must be on the next line. + +### Correct example: +```kotlin +@Nested internal inner class +`some descriptive name` { + // ... +} +``` + +### Incorrect example: +```kotlin +@Nested +internal inner class `some descriptive name` { + // ... +} +``` diff --git a/base/src/test/kotlin/io/spine/base/RejectionTypeKtSpec.kt b/base/src/test/kotlin/io/spine/base/RejectionTypeKtSpec.kt new file mode 100644 index 0000000000..04da4e1e29 --- /dev/null +++ b/base/src/test/kotlin/io/spine/base/RejectionTypeKtSpec.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.base + +import io.kotest.matchers.shouldBe +import io.spine.code.java.SimpleClassName +import io.spine.test.base.rejections.TestRejections.FlyingObjectUnidentified +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`RejectionType` should") +class RejectionTypeKtSpec { + + @Test + fun `recognize a rejection type`() { + val descriptor = FlyingObjectUnidentified.getDescriptor() + RejectionType.test(descriptor) shouldBe true + + val type = RejectionType(descriptor) + type.throwableClass().value() shouldBe + "io.spine.test.base.rejections.FlyingObjectUnidentified" + } + + @Test + fun `verify outer class name`() { + RejectionType.isValidOuterClassName(SimpleClassName.create("MyRejections")) shouldBe true + RejectionType.isValidOuterClassName(SimpleClassName.create("MyEvents")) shouldBe false + } +} diff --git a/base/src/test/kotlin/io/spine/base/ThrowableExtsTest.kt b/base/src/test/kotlin/io/spine/base/ThrowableExtsTest.kt index a1ae11dc75..8bb649a283 100644 --- a/base/src/test/kotlin/io/spine/base/ThrowableExtsTest.kt +++ b/base/src/test/kotlin/io/spine/base/ThrowableExtsTest.kt @@ -42,8 +42,8 @@ import org.junit.jupiter.api.Test internal class `Extensions for 'Throwable' should` { - @Nested - inner class `tell if it was caused by a rejection` { + @Nested inner class + `tell if it was caused by a rejection` { private var throwable: Throwable = RuntimeException() diff --git a/base/src/test/kotlin/io/spine/code/fs/AbstractSourceFileKtSpec.kt b/base/src/test/kotlin/io/spine/code/fs/AbstractSourceFileKtSpec.kt new file mode 100644 index 0000000000..0d93c5ecdd --- /dev/null +++ b/base/src/test/kotlin/io/spine/code/fs/AbstractSourceFileKtSpec.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.code.fs + +import com.google.common.collect.ImmutableList +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldContainExactly +import java.nio.file.Files +import java.nio.file.Path +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +@DisplayName("`AbstractSourceFile` should") +class AbstractSourceFileKtSpec { + + private class StubSourceFile(path: Path) : AbstractSourceFile(path) { + public override fun load() = super.load() + public override fun store() = super.store() + public fun setLines(lines: List) = update(ImmutableList.copyOf(lines)) + public fun getLines() = lines() + } + + @Test + fun `return empty lines if not loaded`(@TempDir tempDir: Path) { + val file = StubSourceFile(tempDir.resolve("non-existent")) + file.getLines().shouldBeEmpty() + } + + @Test + fun `load lines from file`(@TempDir tempDir: Path) { + val path = tempDir.resolve("test.txt") + val content = listOf("line 1", "line 2") + Files.write(path, content) + + val file = StubSourceFile(path) + file.load() + file.getLines() shouldContainExactly content + } + + @Test + fun `store lines to file`(@TempDir tempDir: Path) { + val path = tempDir.resolve("test.txt") + Files.createFile(path) + val content = listOf("line 1", "line 2") + + val file = StubSourceFile(path) + file.setLines(content) + file.store() + + Files.readAllLines(path) shouldContainExactly content + } +} diff --git a/base/src/test/kotlin/io/spine/code/proto/FieldContextKtSpec.kt b/base/src/test/kotlin/io/spine/code/proto/FieldContextKtSpec.kt new file mode 100644 index 0000000000..aa664935cb --- /dev/null +++ b/base/src/test/kotlin/io/spine/code/proto/FieldContextKtSpec.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.code.proto + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.spine.test.base.rejections.TestRejections.FlyingObjectUnidentified +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`FieldContext` should") +class FieldContextKtSpec { + + @Test + fun `create an empty context`() { + val context = FieldContext.empty() + context shouldNotBe null + context.fieldPath().fieldNameList.size shouldBe 0 + } + + @Test + fun `create context for a field`() { + val field = FlyingObjectUnidentified.getDescriptor().findFieldByName("plus_code") + val context = FieldContext.create(field) + + context.target() shouldBe field + context.fieldPath().fieldNameList.size shouldBe 1 + context.fieldPath().getFieldName(0) shouldBe "plus_code" + } + + @Test + fun `create context for a child field`() { + val field = FlyingObjectUnidentified.getDescriptor().findFieldByName("plus_code") + val parent = FieldContext.empty() + val child = parent.forChild(field) + + child.target() shouldBe field + child.fieldPath().fieldNameList.size shouldBe 1 + } + + @Test + fun `compare contexts`() { + val field = FlyingObjectUnidentified.getDescriptor().findFieldByName("plus_code") + val c1 = FieldContext.create(field) + val c2 = FieldContext.create(field) + + c1 shouldBe c2 + c1.hashCode() shouldBe c2.hashCode() + } +} diff --git a/base/src/test/kotlin/io/spine/code/proto/FieldDeclarationSpec.kt b/base/src/test/kotlin/io/spine/code/proto/FieldDeclarationSpec.kt index 848651badd..54b64592cc 100644 --- a/base/src/test/kotlin/io/spine/code/proto/FieldDeclarationSpec.kt +++ b/base/src/test/kotlin/io/spine/code/proto/FieldDeclarationSpec.kt @@ -87,9 +87,8 @@ internal class FieldDeclarationSpec { .testEquals() } - @Nested - @DisplayName("check default values of type") - internal inner class Defaults { + @Nested internal inner class + `check default values of type` { @Test fun int32() { @@ -102,8 +101,7 @@ internal class FieldDeclarationSpec { } @Test - @DisplayName("`string`") - fun aString() { + fun `string`() { val stringField = StringValue.getDescriptor().fields[0] val declaration = FieldDeclaration(stringField) @@ -112,8 +110,7 @@ internal class FieldDeclarationSpec { } @Test - @DisplayName("`Message`") - fun aMessage() { + fun `Message`() { val messageField = Uri.getDescriptor().findFieldByName("auth") val declaration = FieldDeclaration(messageField) @@ -123,9 +120,8 @@ internal class FieldDeclarationSpec { } } - @Nested - @DisplayName("obtain Java type name of") - internal inner class TypeName { + @Nested internal inner class + `obtain Java type name of` { @Test fun int64() { @@ -146,8 +142,7 @@ internal class FieldDeclarationSpec { } @Test - @DisplayName("`Message`") - fun message() { + fun `Message`() { val messageField = Uri.getDescriptor().findFieldByName("protocol") val declaration = FieldDeclaration(messageField) val typeName = declaration.javaTypeName() @@ -156,8 +151,7 @@ internal class FieldDeclarationSpec { } @Test - @DisplayName("`enum`") - fun anEnum() { + fun `enum`() { val enumField = Uri.Protocol.getDescriptor().findFieldByName("schema") val declaration = FieldDeclaration(enumField) val typeName = declaration.javaTypeName() diff --git a/base/src/test/kotlin/io/spine/code/proto/FileNameSpec.kt b/base/src/test/kotlin/io/spine/code/proto/FileNameSpec.kt index f4419b0150..efd1b54e61 100644 --- a/base/src/test/kotlin/io/spine/code/proto/FileNameSpec.kt +++ b/base/src/test/kotlin/io/spine/code/proto/FileNameSpec.kt @@ -63,9 +63,8 @@ internal class FileNameSpec { FileName.of("many_more_rejections.proto").nameOnlyCamelCase() shouldBe "ManyMoreRejections" } - @Nested - @DisplayName("Calculate outer class name") - internal inner class OuterClassName { + @Nested internal inner class + `calculating outer class name` { @Test fun `one word name`() { @@ -104,8 +103,7 @@ internal class FileNameSpec { } @Test - @DisplayName("tell events file kind") - fun eventsFile() { + fun `tell events file kind`() { val eventsFile = FileName.of("project_events.proto") eventsFile.isEvents shouldBe true @@ -114,8 +112,7 @@ internal class FileNameSpec { } @Test - @DisplayName("tell rejection file kind") - fun rejectionsFile() { + fun `tell rejection file kind`() { val rejectionsFile = FileName.of("rejections.proto") rejectionsFile.isRejections shouldBe true diff --git a/base/src/test/kotlin/io/spine/code/proto/MessageTypeSpec.kt b/base/src/test/kotlin/io/spine/code/proto/MessageTypeSpec.kt index c5c8305512..97cac96376 100644 --- a/base/src/test/kotlin/io/spine/code/proto/MessageTypeSpec.kt +++ b/base/src/test/kotlin/io/spine/code/proto/MessageTypeSpec.kt @@ -60,9 +60,8 @@ import org.junit.jupiter.api.Test @DisplayName("`MessageType` should") internal class MessageTypeSpec { - @Nested - @DisplayName("tell if a type") - internal inner class Tell { + @Nested internal inner class + `tell if a type` { /** * Tests a certain boolean method of `MessageType` created on the passed descriptor. @@ -73,8 +72,8 @@ internal class MessageTypeSpec { result shouldBe true } - @Nested - internal inner class Is { + @Nested internal inner class + `be` { @Test fun nested() { @@ -117,9 +116,8 @@ internal class MessageTypeSpec { * This test suite takes nested types of corresponding signals to * verify that they are not seen as signals of the kind of the enclosing types. */ - @Nested - @DisplayName("not") - internal inner class NotA { + @Nested internal inner class + `not` { @Test fun `a rejection`() { @@ -148,9 +146,8 @@ internal class MessageTypeSpec { } } - @Nested - @DisplayName("a non-Google or a Spine options type") - internal inner class Custom { + @Nested internal inner class + `a non-Google or a Spine options type` { @Test fun `positively for a custom type`() { @@ -172,9 +169,8 @@ internal class MessageTypeSpec { } } - @Nested - @DisplayName("obtain a path for") - internal inner class Path { + @Nested internal inner class + `obtain a path for` { @CanIgnoreReturnValue private fun assertPath(descriptor: Descriptor): IterableSubject { diff --git a/base/src/test/kotlin/io/spine/code/proto/PackageNameSpec.kt b/base/src/test/kotlin/io/spine/code/proto/PackageNameSpec.kt index 610d2834fb..7fdfff2699 100644 --- a/base/src/test/kotlin/io/spine/code/proto/PackageNameSpec.kt +++ b/base/src/test/kotlin/io/spine/code/proto/PackageNameSpec.kt @@ -38,7 +38,7 @@ import org.junit.jupiter.api.Test internal class PackageNameSpec { @Test - fun handleNullArgs() { + fun `handle 'null' arguments`() { NullPointerTester().testAllPublicStaticMethods(PackageName::class.java) } @@ -53,9 +53,8 @@ internal class PackageNameSpec { PackageName.of(packageName).value() shouldBe packageName } - @Nested - @DisplayName("verify if the package is inner to a parent package") - internal inner class SubPackage { + @Nested internal inner class + `verify if the package is inner to a parent package` { @Test fun `if immediately nested`() { diff --git a/base/src/test/kotlin/io/spine/code/proto/TypeSetKtSpec.kt b/base/src/test/kotlin/io/spine/code/proto/TypeSetKtSpec.kt new file mode 100644 index 0000000000..e1d27028ef --- /dev/null +++ b/base/src/test/kotlin/io/spine/code/proto/TypeSetKtSpec.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.code.proto + +import io.kotest.matchers.shouldBe +import io.spine.test.base.rejections.TestRejections.FlyingObjectUnidentified +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`TypeSet` should") +class TypeSetKtSpec { + + @Test + fun `create an empty set`() { + val set = TypeSet.newBuilder().build() + set.isEmpty() shouldBe true + set.size() shouldBe 0 + } + + @Test + fun `create from a file descriptor`() { + val descriptor = FlyingObjectUnidentified.getDescriptor().file + val set = TypeSet.from(descriptor) + + set.isEmpty() shouldBe false + set.messageTypes().size shouldBe 1 + } + + @Test + fun `unite sets`() { + val descriptor = FlyingObjectUnidentified.getDescriptor().file + val set1 = TypeSet.from(descriptor) + val set2 = TypeSet.newBuilder().build() + val union = set1.union(set2) + + union shouldBe set1 + } +} diff --git a/base/src/test/kotlin/io/spine/io/EnsureSpec.kt b/base/src/test/kotlin/io/spine/io/EnsureSpec.kt index d502baa994..eea2ae369d 100644 --- a/base/src/test/kotlin/io/spine/io/EnsureSpec.kt +++ b/base/src/test/kotlin/io/spine/io/EnsureSpec.kt @@ -44,9 +44,8 @@ import org.junit.jupiter.api.io.TempDir @DisplayName("`Ensure` utilities class should") internal class EnsureSpec : UtilityClassTest(Ensure::class.java) { - @Nested - @DisplayName("handle files via") - internal inner class OnFiles { + @Nested internal inner class + `handle files via` { private lateinit var file: File @@ -74,9 +73,8 @@ internal class EnsureSpec : UtilityClassTest(Ensure::class.java) { } } - @Nested - @DisplayName("handle a directory creation") - internal inner class OnDirectories { + @Nested internal inner class + `handle a directory creation` { private lateinit var tempDir: Path @@ -102,8 +100,7 @@ internal class EnsureSpec : UtilityClassTest(Ensure::class.java) { } @Test - @DisplayName("if it exists") - fun existing() { + fun `if it exists`() { val existingDir = tempDir.resolve(TestValues.randomString()) ensureDirectory(existingDir) diff --git a/base/src/test/kotlin/io/spine/io/Files2Spec.kt b/base/src/test/kotlin/io/spine/io/Files2Spec.kt index 13f07d2de5..9b42062fe7 100644 --- a/base/src/test/kotlin/io/spine/io/Files2Spec.kt +++ b/base/src/test/kotlin/io/spine/io/Files2Spec.kt @@ -49,9 +49,8 @@ internal class Files2Spec : UtilityClassTest(Files2::class.java) { testFolder = testFolderPath.toFile() } - @Nested - @DisplayName("verify that an existing file is not empty") - internal inner class NonEmptyFile { + @Nested internal inner class + `verify that an existing file is not empty` { @Test fun `returning 'false' when existing file is empty`() { diff --git a/base/src/test/kotlin/io/spine/io/GlobSpec.kt b/base/src/test/kotlin/io/spine/io/GlobSpec.kt index 972d385479..a493ce0501 100644 --- a/base/src/test/kotlin/io/spine/io/GlobSpec.kt +++ b/base/src/test/kotlin/io/spine/io/GlobSpec.kt @@ -42,8 +42,8 @@ class GlobSpec { assertThrows { Glob("") } } - @Nested - inner class `create instances by extension which` { + @Nested inner class + `create instances by extension which` { @Test fun `is empty`() { diff --git a/base/src/test/kotlin/io/spine/protobuf/DescriptorExtsSpec.kt b/base/src/test/kotlin/io/spine/protobuf/DescriptorExtsSpec.kt index 43dd18a202..054052dd01 100644 --- a/base/src/test/kotlin/io/spine/protobuf/DescriptorExtsSpec.kt +++ b/base/src/test/kotlin/io/spine/protobuf/DescriptorExtsSpec.kt @@ -42,9 +42,8 @@ import org.junit.jupiter.api.TestInstance.Lifecycle @DisplayName("`Descriptor` extensions from `io.spine.protobuf` should") internal class DescriptorExtsSpec { - @Nested - @DisplayName("provide a field descriptor by") - inner class FieldDescriptor { + @Nested inner class + `provide a field descriptor by` { private val descriptor = Timestamp.getDescriptor() diff --git a/base/src/test/kotlin/io/spine/protobuf/DiffKtSpec.kt b/base/src/test/kotlin/io/spine/protobuf/DiffKtSpec.kt new file mode 100644 index 0000000000..f152ee63dc --- /dev/null +++ b/base/src/test/kotlin/io/spine/protobuf/DiffKtSpec.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.protobuf + +import io.kotest.matchers.shouldBe +import io.spine.code.proto.FieldDeclaration +import io.spine.test.base.rejections.flyingObjectUnidentified +import io.spine.test.base.rejections.TestRejections.FlyingObjectUnidentified +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +@DisplayName("`Diff` should") +class DiffKtSpec { + + @Test + fun `calculate difference between two messages of the same type`() { + val m1 = flyingObjectUnidentified { + plusCode = "ABC" + } + val m2 = flyingObjectUnidentified { + plusCode = "DEF" + } + + val diff = Diff.between(m1, m2) + val field = FieldDeclaration(FlyingObjectUnidentified.getDescriptor() + .findFieldByName("plus_code")) + + diff.contains(field) shouldBe true + } + + @Test + fun `return empty diff for same messages`() { + val m1 = flyingObjectUnidentified { + plusCode = "ABC" + } + + val diff = Diff.between(m1, m1) + val field = FieldDeclaration(FlyingObjectUnidentified.getDescriptor() + .findFieldByName("plus_code")) + + diff.contains(field) shouldBe false + } + + @Test + fun `throw IAE if message types are different`() { + val m1 = FlyingObjectUnidentified.getDefaultInstance() + val m2 = io.spine.base.Error.getDefaultInstance() + + assertThrows { + Diff.between(m1, m2) + } + } +} diff --git a/base/src/test/kotlin/io/spine/protobuf/Durations2Spec.kt b/base/src/test/kotlin/io/spine/protobuf/Durations2Spec.kt index 8b7bd50d63..d64fdbba8e 100644 --- a/base/src/test/kotlin/io/spine/protobuf/Durations2Spec.kt +++ b/base/src/test/kotlin/io/spine/protobuf/Durations2Spec.kt @@ -86,9 +86,8 @@ internal class Durations2Spec : UtilityClassTest(Durations2::class.j toJavaTime(original) shouldBe backward.convert(original) } - @Nested - @DisplayName("provide DSL methods for") - internal inner class Dsl { + @Nested internal inner class + `provide DSL methods for` { private var value: Long = 0 @@ -129,9 +128,8 @@ internal class Durations2Spec : UtilityClassTest(Durations2::class.j } } - @Nested - @DisplayName("convert a number of hours") - internal inner class HourConversion { + @Nested internal inner class + `convert a number of hours` { private fun test(hours: Long) { val expected = seconds(hoursToSeconds(hours)) @@ -156,9 +154,8 @@ internal class Durations2Spec : UtilityClassTest(Durations2::class.j } } - @Nested - @DisplayName("fail if") - internal inner class MathError { + @Nested internal inner class + `fail if` { @Test fun `hours value is too big`() { @@ -175,19 +172,16 @@ internal class Durations2Spec : UtilityClassTest(Durations2::class.j } } - @Nested - @DisplayName("add") - internal inner class Add { + @Nested internal inner class + `add` { @Test - @DisplayName("two `null`s -> `ZERO`") - fun nullPlusNull() { + fun `two 'null's to 'ZERO'`() { add(null, null) shouldBe Durations.ZERO } @Test - @DisplayName("`null` returning same instance") - fun sameWithNull() { + fun `'null' returning same instance`() { val duration = seconds(525) add(duration, null) shouldBeSameInstanceAs duration @@ -221,9 +215,8 @@ internal class Durations2Spec : UtilityClassTest(Durations2::class.j } } - @Nested - @DisplayName("Obtain from `Duration`") - internal inner class Obtain { + @Nested internal inner class + `Obtain from 'Duration'` { @Test fun `amount of hours`() { @@ -233,9 +226,8 @@ internal class Durations2Spec : UtilityClassTest(Durations2::class.j } } - @Nested - @DisplayName("verify if `Duration` is") - internal inner class Verify { + @Nested internal inner class + `verify if 'Duration' is` { @Test fun `positive or zero`() { @@ -252,9 +244,8 @@ internal class Durations2Spec : UtilityClassTest(Durations2::class.j } } - @Nested - @DisplayName("tell if `Duration` is") - internal inner class Compare { + @Nested internal inner class + `tell if 'Duration' is` { @Test fun greater() { diff --git a/base/src/test/kotlin/io/spine/security/InvocationGuardTest.kt b/base/src/test/kotlin/io/spine/security/InvocationGuardTest.kt index d122be6edd..17dbc7115a 100644 --- a/base/src/test/kotlin/io/spine/security/InvocationGuardTest.kt +++ b/base/src/test/kotlin/io/spine/security/InvocationGuardTest.kt @@ -36,9 +36,8 @@ import org.junit.jupiter.api.function.Executable @DisplayName("`InvocationGuard` should") internal class InvocationGuardTest { - @Nested - @DisplayName("throw `SecurityException`") - internal inner class Throwing { + @Nested internal inner class + `throw 'SecurityException'` { @Test fun `if no classes are allowed`() { diff --git a/base/src/test/kotlin/io/spine/string/StringifiersSpec.kt b/base/src/test/kotlin/io/spine/string/StringifiersSpec.kt index 81b43cd8be..2ef53e7ad5 100644 --- a/base/src/test/kotlin/io/spine/string/StringifiersSpec.kt +++ b/base/src/test/kotlin/io/spine/string/StringifiersSpec.kt @@ -39,6 +39,8 @@ import io.spine.base.Time import io.spine.string.Stringifiers.stringify import io.spine.test.string.STask import io.spine.test.string.STaskId +import io.spine.test.string.sTask +import io.spine.test.string.sTaskId import io.spine.test.string.STaskStatus import io.spine.testing.UtilityClassTest import io.spine.type.toCompactJson @@ -57,9 +59,8 @@ internal class StringifiersSpec : UtilityClassTest(Stringifiers::c private const val SIZE = 5 } - @Nested - @DisplayName("stringify") - internal inner class StringifyKt { + @Nested internal inner class + `stringify` { @Test fun boolean() = checkStringifies(false, "false") @@ -97,13 +98,13 @@ internal class StringifiersSpec : UtilityClassTest(Stringifiers::c @Test fun `a Protobuf 'Message'`() { - val id = STaskId.newBuilder() - .setUuid(Identifier.newUuid()) - .build() - val message = STask.newBuilder() - .setId(id) - .setStatus(STaskStatus.DONE) - .build() + val id = sTaskId { + uuid = Identifier.newUuid() + } + val message = sTask { + this.id = id + status = STaskStatus.DONE + } val expected = message.toCompactJson() checkStringifies(message, expected) @@ -114,9 +115,8 @@ internal class StringifiersSpec : UtilityClassTest(Stringifiers::c } } - @Nested - @DisplayName("create 'Stringifier' with a delimeter for") - internal inner class Delimited { + @Nested internal inner class + `create 'Stringifier' with a delimeter for` { @Test fun List() { @@ -169,9 +169,8 @@ internal class StringifiersSpec : UtilityClassTest(Stringifiers::c * This class covers only cases that are not touched by other tests that * involve parsing of string values. */ - @Nested - @DisplayName("parse a string into") - internal inner class Parsing { + @Nested internal inner class + `parse a string into` { @Test fun Boolean() { diff --git a/base/src/test/kotlin/io/spine/type/ApiOptionSpec.kt b/base/src/test/kotlin/io/spine/type/ApiOptionSpec.kt index 81dee8339a..a07431627f 100644 --- a/base/src/test/kotlin/io/spine/type/ApiOptionSpec.kt +++ b/base/src/test/kotlin/io/spine/type/ApiOptionSpec.kt @@ -51,9 +51,8 @@ import org.junit.jupiter.api.Test @DisplayName("`ApiOption` should") class ApiOptionSpec { - @Nested - @DisplayName("provide items for API stability annotations") - inner class ApiStabilityEnum { + @Nested inner class + `provide items for API stability annotations` { @Test fun beta() { @@ -85,9 +84,8 @@ class ApiOptionSpec { } } - @Nested - @DisplayName("locate itself in descriptors") - inner class LocatingInDescriptor { + @Nested inner class + `locate itself in descriptors` { @Test fun beta() { diff --git a/base/src/test/kotlin/io/spine/type/DescriptorExtsSpec.kt b/base/src/test/kotlin/io/spine/type/DescriptorExtsSpec.kt index ee4b043673..78c5f6a28e 100644 --- a/base/src/test/kotlin/io/spine/type/DescriptorExtsSpec.kt +++ b/base/src/test/kotlin/io/spine/type/DescriptorExtsSpec.kt @@ -46,9 +46,8 @@ import org.junit.jupiter.api.Test @DisplayName("`Descriptor` extensions from `io.spine.type` should") internal class DescriptorExtsSpec { - @Nested - @DisplayName("tell that a message type is marked as") - inner class ExplicitOption { + @Nested inner class + `tell that a message type is marked as` { @Test fun beta() { diff --git a/base/src/test/kotlin/io/spine/type/FileDescriptorExtsSpec.kt b/base/src/test/kotlin/io/spine/type/FileDescriptorExtsSpec.kt index 2509470823..92416bae11 100644 --- a/base/src/test/kotlin/io/spine/type/FileDescriptorExtsSpec.kt +++ b/base/src/test/kotlin/io/spine/type/FileDescriptorExtsSpec.kt @@ -46,9 +46,8 @@ import org.junit.jupiter.api.Test @DisplayName("`FileDescriptor` extensions from `io.spine.type` should") internal class FileDescriptorExtsSpec { - @Nested - @DisplayName("tell if all types in a file are") - inner class AllTypesAre { + @Nested inner class + `tell if all types in a file are` { @Test fun beta() { diff --git a/base/src/test/kotlin/io/spine/type/JsonSpec.kt b/base/src/test/kotlin/io/spine/type/JsonSpec.kt index fd87c115c3..8b02abd7ed 100644 --- a/base/src/test/kotlin/io/spine/type/JsonSpec.kt +++ b/base/src/test/kotlin/io/spine/type/JsonSpec.kt @@ -31,6 +31,7 @@ import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.spine.base.Identifier import io.spine.json.given.Node +import io.spine.json.given.node import io.spine.json.given.WrappedString import io.spine.testing.Assertions.assertNpe import io.spine.testing.TestValues @@ -55,10 +56,10 @@ internal class JsonSpec { @Test fun `print to compact JSON`() { val idValue = Identifier.newUuid() - val node = Node.newBuilder() - .setName(idValue) - .setRight(Node.getDefaultInstance()) - .build() + val node = node { + name = idValue + right = Node.getDefaultInstance() + } val result = node.toCompactJson() result shouldNotBe "" diff --git a/base/src/test/kotlin/io/spine/type/KnownTypesSpec.kt b/base/src/test/kotlin/io/spine/type/KnownTypesSpec.kt index cb1760f0fb..3fdd08cd42 100644 --- a/base/src/test/kotlin/io/spine/type/KnownTypesSpec.kt +++ b/base/src/test/kotlin/io/spine/type/KnownTypesSpec.kt @@ -106,9 +106,8 @@ internal class KnownTypesSpec { found.isEmpty() shouldBe false } - @Nested - @DisplayName("contain types") - internal inner class ContainTypes { + @Nested internal inner class + `contain types` { @Test fun `defined by Spine framework`() { @@ -181,8 +180,7 @@ internal class KnownTypesSpec { } @Test - @DisplayName("throw UnknownTypeException for requesting info on an unknown type") - fun throwOnUnknownType() { + fun `throw 'UnknownTypeException' for requesting info on an unknown type`() { val unexpectedUrl = TypeUrl.parse("prefix/unexpected.type") assertUnknownType { knownTypes.classNameOf(unexpectedUrl) } } diff --git a/base/src/test/kotlin/io/spine/type/MessageExtsSpec.kt b/base/src/test/kotlin/io/spine/type/MessageExtsSpec.kt index aa2ddb9a06..e9027e5a6e 100644 --- a/base/src/test/kotlin/io/spine/type/MessageExtsSpec.kt +++ b/base/src/test/kotlin/io/spine/type/MessageExtsSpec.kt @@ -45,9 +45,8 @@ internal class MessageExtsSpec { "spine.given.type.ExplicitInternalType" } - @Nested - @DisplayName("tell if a message is internal") - inner class InternalMessage { + @Nested inner class + `tell if a message is internal` { @Test fun `by class annotation`() { diff --git a/base/src/test/kotlin/io/spine/type/ProtoTextExtsSpec.kt b/base/src/test/kotlin/io/spine/type/ProtoTextExtsSpec.kt index 613d1455d7..ef124c8071 100644 --- a/base/src/test/kotlin/io/spine/type/ProtoTextExtsSpec.kt +++ b/base/src/test/kotlin/io/spine/type/ProtoTextExtsSpec.kt @@ -48,9 +48,8 @@ import org.junit.jupiter.api.Test @DisplayName("Kotlin extensions for proto text output should") internal class ProtoTextExtsSpec { - @Nested - @DisplayName("print short debug string of") - inner class DebugStrOutput { + @Nested inner class + `print short debug string of` { private lateinit var msg: Timestamp private lateinit var debugStr: String @@ -88,9 +87,8 @@ internal class ProtoTextExtsSpec { } } - @Nested - @DisplayName("print proto text output with name which") - inner class TextOutput { + @Nested inner class + `print proto text output with name which` { private lateinit var msg: MapOfAnys private lateinit var textOut: String diff --git a/base/src/test/kotlin/io/spine/type/ServiceDescriptorExtsSpec.kt b/base/src/test/kotlin/io/spine/type/ServiceDescriptorExtsSpec.kt index 91c18dff91..a50936ead0 100644 --- a/base/src/test/kotlin/io/spine/type/ServiceDescriptorExtsSpec.kt +++ b/base/src/test/kotlin/io/spine/type/ServiceDescriptorExtsSpec.kt @@ -37,9 +37,8 @@ import org.junit.jupiter.api.Test @DisplayName("`ServiceDescriptor` extensions in `io.spine.type` should") internal class ServiceDescriptorExtsSpec { - @Nested - @DisplayName("tell if a service is") - inner class SpiAnnotations { + @Nested inner class + `tell if a service is` { @Test fun `explicitly annotated SPI`() { diff --git a/base/src/test/kotlin/io/spine/type/TypeUrlSpec.kt b/base/src/test/kotlin/io/spine/type/TypeUrlSpec.kt index 2436a20604..fa7d25a54d 100644 --- a/base/src/test/kotlin/io/spine/type/TypeUrlSpec.kt +++ b/base/src/test/kotlin/io/spine/type/TypeUrlSpec.kt @@ -29,6 +29,7 @@ package io.spine.type import com.google.common.testing.EqualsTester import com.google.common.testing.SerializableTester import com.google.protobuf.Any +import com.google.protobuf.any import com.google.protobuf.AnyProto import com.google.protobuf.BoolValue import com.google.protobuf.Descriptors.Descriptor @@ -77,7 +78,7 @@ internal class TypeUrlSpec { `create an instance by` { @Test - fun message() { + fun `a message`() { val msg = TypeConverter.toMessage(Identifier.newUuid()) val typeUrl = TypeUrl.of(msg) assertTypeUrl(typeUrl) @@ -188,8 +189,7 @@ internal class TypeUrlSpec { } @Test - @DisplayName("created for a type declared in a file with empty `(type_url_prefix)`") - fun inFile() { + fun `created for a type declared in a file with empty '(type_url_prefix)'`() { noPrefixType = TypeUrl.from(TypeWithoutPrefix.getDescriptor()) assertEmptyPrefix() } @@ -214,9 +214,9 @@ internal class TypeUrlSpec { @Test fun `invalid URL of a packed message`() { - val any = Any.newBuilder() - .setTypeUrl("invalid_type_url") - .build() + val any = any { + typeUrl = "invalid_type_url" + } val exception = assertThrows { TypeUrl.ofEnclosed(any) } @@ -319,7 +319,7 @@ internal class TypeUrlSpec { } @Test - fun serialize() { + fun `be serializable`() { SerializableTester.reserializeAndAssert(TypeUrl.of(Timestamp::class.java)) } } diff --git a/base/src/test/kotlin/io/spine/util/Preconditions2Test.kt b/base/src/test/kotlin/io/spine/util/Preconditions2Test.kt index 18488dd0be..788092d0f9 100644 --- a/base/src/test/kotlin/io/spine/util/Preconditions2Test.kt +++ b/base/src/test/kotlin/io/spine/util/Preconditions2Test.kt @@ -86,8 +86,8 @@ internal class Preconditions2Test : UtilityClassTest(Preconditio } } - @Nested - internal inner class `check that a 'String' is` : + @Nested internal inner class + `check that a 'String' is` : TestSuite(Preconditions2::checkNotEmptyOrBlank, { arg, fmt, params -> checkNotEmptyOrBlank(arg, fmt, params) }) { @@ -129,8 +129,8 @@ internal class Preconditions2Test : UtilityClassTest(Preconditio } - @Nested - internal inner class `check that a value is positive` : + @Nested internal inner class + `check that a value is positive` : TestSuite(Preconditions2::checkPositive,{ arg, fmt, params -> checkPositive(arg, fmt, params) }) { @@ -154,8 +154,8 @@ internal class Preconditions2Test : UtilityClassTest(Preconditio } } - @Nested - internal inner class `check that a value is positive or zero` : + @Nested internal inner class + `check that a value is positive or zero` : TestSuite(Preconditions2::checkNonNegative, { arg, fmt, params -> checkNonNegative(arg, fmt, params) }) { @@ -183,9 +183,8 @@ internal class Preconditions2Test : UtilityClassTest(Preconditio assertIllegalArgument { checkBounds(10, "checked value", -5, 9) } } - @Nested - @DisplayName("check that a message is not in the default state") - internal inner class NotDefaultMessage { + @Nested internal inner class + `check that a message is not in the default state` { private val defaultValue: Message = StringValue.getDefaultInstance() private var customErrorMessage: String? = null diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md index 14cc57c8a4..5c37a7951c 100644 --- a/docs/dependencies/dependencies.md +++ b/docs/dependencies/dependencies.md @@ -764,7 +764,7 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 01 10:59:29 WEST 2026** using +This report was generated on **Tue Jun 02 22:17:18 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -1616,7 +1616,7 @@ This report was generated on **Mon Jun 01 10:59:29 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 10:59:29 WEST 2026** using +This report was generated on **Tue Jun 02 22:17:18 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -2446,7 +2446,7 @@ This report was generated on **Mon Jun 01 10:59:29 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 10:59:29 WEST 2026** using +This report was generated on **Tue Jun 02 22:17:18 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -3356,6 +3356,6 @@ This report was generated on **Mon Jun 01 10:59:29 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 10:59:29 WEST 2026** using +This report was generated on **Tue Jun 02 22:17:18 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file From b21a69f782eba685198909b284ed7680ee9ff655 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 2 Jun 2026 22:48:18 +0100 Subject: [PATCH 20/45] Add tests Also: * Update test-related memories. --- .agents/memory/MEMORY.md | 2 + .../kotlin/io/spine/code/proto/FileSetSpec.kt | 155 ++++++++++++++++ .../io/spine/io/{CopyTest.kt => CopySpec.kt} | 6 +- ...ectoryTest.kt => ResourceDirectorySpec.kt} | 4 +- .../io/spine/query/CartesianProductsSpec.kt | 165 ++++++++++++++++++ .../kotlin/io/spine/query/EntityQuerySpec.kt | 131 ++++++++++++++ 6 files changed, 461 insertions(+), 2 deletions(-) create mode 100644 base/src/test/kotlin/io/spine/code/proto/FileSetSpec.kt rename base/src/test/kotlin/io/spine/io/{CopyTest.kt => CopySpec.kt} (96%) rename base/src/test/kotlin/io/spine/io/{ResourceDirectoryTest.kt => ResourceDirectorySpec.kt} (97%) create mode 100644 base/src/test/kotlin/io/spine/query/CartesianProductsSpec.kt create mode 100644 base/src/test/kotlin/io/spine/query/EntityQuerySpec.kt diff --git a/.agents/memory/MEMORY.md b/.agents/memory/MEMORY.md index 6d811aec84..2cd56e83e3 100644 --- a/.agents/memory/MEMORY.md +++ b/.agents/memory/MEMORY.md @@ -7,6 +7,8 @@ See [README.md](README.md) for the format and routing rules. - [copilot-review-request](feedback/copilot-review-request.md) — GraphQL `requestReviews` with `botIds: ["BOT_kgDOCnlnWA"]`; REST endpoint silently no-ops on re-requests. - [kotlin-test-formatting](feedback/kotlin-test-formatting.md) — `@Nested` should be on the same line as `inner class`, and backticked name on the next line. +- [equals-tester](feedback/equals-tester.md) — Use Guava's `EqualsTester` for testing `equals()` and `hashCode()`. +- [utility-class-testing](feedback/utility-class-testing.md) — Use `UtilityClassTest` as the base for testing utility classes. ## Project (durable context & rationale) diff --git a/base/src/test/kotlin/io/spine/code/proto/FileSetSpec.kt b/base/src/test/kotlin/io/spine/code/proto/FileSetSpec.kt new file mode 100644 index 0000000000..f5d88182e5 --- /dev/null +++ b/base/src/test/kotlin/io/spine/code/proto/FileSetSpec.kt @@ -0,0 +1,155 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.code.proto + +import com.google.common.testing.EqualsTester +import com.google.protobuf.DescriptorProtos.FileDescriptorProto +import com.google.protobuf.Empty +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.ints.shouldBeLessThan +import io.kotest.matchers.optional.shouldBePresent +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.shouldContain +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +@DisplayName("`FileSet` should") +internal class FileSetSpec { + + private lateinit var fileSet: FileSet + + @BeforeEach + fun loadFileSet() { + fileSet = FileSet.load() + } + + @Nested internal inner class + `verify if it contains a file` { + + @Test + fun `by its name`() { + val name = FileName.from(Empty.getDescriptor().file) + fileSet.contains(name) shouldBe true + } + + @Test + fun `by a collection of names`() { + val name = FileName.from(Empty.getDescriptor().file) + fileSet.containsAll(listOf(name)) shouldBe true + } + } + + @Nested internal inner class + `find a file` { + + @Test + fun `by its name`() { + val name = FileName.from(Empty.getDescriptor().file) + fileSet.tryFind(name).shouldBePresent() + } + + @Test + fun `by a collection of names`() { + val name = FileName.from(Empty.getDescriptor().file) + val found = fileSet.find(listOf(name)) + found.size() shouldBe 1 + } + } + + @Test + fun `be empty when newly created`() { + val emptySet = FileSet.newInstance() + emptySet.isEmpty shouldBe true + emptySet.size() shouldBe 0 + } + + @Test + fun `create a union with another set`() { + val emptySet = FileSet.newInstance() + fileSet.union(emptySet) shouldBe fileSet + emptySet.union(fileSet) shouldBe fileSet + + val anotherSet = FileSet.newInstance() + val file = Empty.getDescriptor().file + anotherSet.add(file) + + val union = emptySet.union(anotherSet) + union.size() shouldBe 1 + union.contains(FileName.from(file)) shouldBe true + } + + @Test + fun `filter files by predicate`() { + val filtered = fileSet.filter { it.fullName.contains("empty") } + filtered.files().forEach { + it.fullName.contains("empty") shouldBe true + } + } + + @Test + fun `convert to array`() { + val array = fileSet.toArray() + array.size shouldBe fileSet.size() + } + + @Test + fun `support 'equals()' and 'hashCode()'`() { + val file = Empty.getDescriptor().file + val set1 = FileSet.newInstance() + set1.add(file) + val set2 = FileSet.newInstance() + set2.add(file) + + EqualsTester() + .addEqualityGroup(set1, set2) + .addEqualityGroup(FileSet.newInstance()) + .addEqualityGroup(fileSet) + .testEquals() + } + + @Test + fun `provide 'toString'`() { + val file1 = Empty.getDescriptor().file + val file2 = com.google.protobuf.Any.getDescriptor().file + val set = FileSet.newInstance() + set.add(file1) + set.add(file2) + + val str = set.toString() + str shouldContain "FileSet" + str shouldContain "files=" + str shouldContain file1.fullName + str shouldContain file2.fullName + + // google/protobuf/any.proto comes before google/protobuf/empty.proto + str.indexOf(file2.fullName) shouldBeLessThan str.indexOf(file1.fullName) + } +} diff --git a/base/src/test/kotlin/io/spine/io/CopyTest.kt b/base/src/test/kotlin/io/spine/io/CopySpec.kt similarity index 96% rename from base/src/test/kotlin/io/spine/io/CopyTest.kt rename to base/src/test/kotlin/io/spine/io/CopySpec.kt index 9423d2adc6..8ea2b8cbc1 100644 --- a/base/src/test/kotlin/io/spine/io/CopyTest.kt +++ b/base/src/test/kotlin/io/spine/io/CopySpec.kt @@ -26,8 +26,10 @@ package io.spine.io import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe import io.spine.io.Copy.copyContent import io.spine.io.Copy.copyDir +import io.spine.testing.UtilityClassTest import java.nio.file.Files.createDirectory import java.nio.file.Files.exists import java.nio.file.Files.write @@ -36,10 +38,12 @@ import java.nio.file.StandardOpenOption.CREATE import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir -class `Copy utilities should` { +@DisplayName("`Copy` utilities should") +internal class CopySpec : UtilityClassTest(Copy::class.java) { companion object { const val sourceDirectory = "to-copy" diff --git a/base/src/test/kotlin/io/spine/io/ResourceDirectoryTest.kt b/base/src/test/kotlin/io/spine/io/ResourceDirectorySpec.kt similarity index 97% rename from base/src/test/kotlin/io/spine/io/ResourceDirectoryTest.kt rename to base/src/test/kotlin/io/spine/io/ResourceDirectorySpec.kt index febe146d93..dd61717b6f 100644 --- a/base/src/test/kotlin/io/spine/io/ResourceDirectoryTest.kt +++ b/base/src/test/kotlin/io/spine/io/ResourceDirectorySpec.kt @@ -34,10 +34,12 @@ import java.util.function.Predicate import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir -class `'ResourceDirectory' should` { +@DisplayName("`ResourceDirectory` should") +internal class ResourceDirectorySpec { private lateinit var directory: ResourceDirectory private lateinit var target: Path diff --git a/base/src/test/kotlin/io/spine/query/CartesianProductsSpec.kt b/base/src/test/kotlin/io/spine/query/CartesianProductsSpec.kt new file mode 100644 index 0000000000..3c1c00658e --- /dev/null +++ b/base/src/test/kotlin/io/spine/query/CartesianProductsSpec.kt @@ -0,0 +1,165 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.query + +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.spine.query.ComparisonOperator +import io.spine.query.given.RecordQueryBuilderTestEnv.ManufacturerColumns.is_traded +import io.spine.query.given.RecordQueryBuilderTestEnv.ManufacturerColumns.stock_count +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +@DisplayName("`CartesianProducts` should") +internal class CartesianProductsSpec { + + @Nested internal inner class + `multiply simple parameters over an 'OrExpression'` { + + @Test + fun `producing a union of conjunctions`() { + val param = RecordSubjectParameter(is_traded, ComparisonOperator.EQUALS, true) + val params = listOf(param) + + val customColumn = object : CustomColumn() { + override fun name(): ColumnName = ColumnName.of("custom") + override fun type(): Class = String::class.java + override fun valueIn(source: Manufacturer): String = "value" + } + val customParam = CustomSubjectParameter(customColumn, "value", ComparisonOperator.EQUALS) + + val orExpression = OrExpression.newBuilder() + .addParam(RecordSubjectParameter(stock_count, ComparisonOperator.EQUALS, 10)) + .addParam(RecordSubjectParameter(stock_count, ComparisonOperator.EQUALS, 20)) + .addCustomParam(customParam) + .addExpression(AndExpression.newBuilder() + .addParam(RecordSubjectParameter(stock_count, ComparisonOperator.EQUALS, 30)) + .build()) + .build() + + val result = OrExpression.newBuilder() + CartesianProducts.cartesianSimpleParams(params, orExpression, result) + + val union = result.build() + union.children() shouldHaveSize 4 + } + } + + @Nested internal inner class + `multiply child expressions over an 'OrExpression'` { + + @Test + fun `producing a union of conjunctions`() { + val child = AndExpression.newBuilder() + .addParam(RecordSubjectParameter(is_traded, ComparisonOperator.EQUALS, true)) + .build() + val children = listOf(child) + + val customColumn = object : CustomColumn() { + override fun name(): ColumnName = ColumnName.of("custom") + override fun type(): Class = String::class.java + override fun valueIn(source: Manufacturer): String = "value" + } + val customParam = CustomSubjectParameter(customColumn, "value", ComparisonOperator.EQUALS) + + val orExpression = OrExpression.newBuilder() + .addParam(RecordSubjectParameter(stock_count, ComparisonOperator.EQUALS, 10)) + .addParam(RecordSubjectParameter(stock_count, ComparisonOperator.EQUALS, 20)) + .addCustomParam(customParam) + .build() + + val result = OrExpression.newBuilder() + CartesianProducts.cartesianChildren(children, orExpression, result) + + val union = result.build() + union.children() shouldHaveSize 3 + union.children().forEach { + it.operator() shouldBe LogicalOperator.AND + } + } + + @Test + fun `producing a union of conjunctions when child is an 'OrExpression'`() { + val innerOr = OrExpression.newBuilder() + .addParam(RecordSubjectParameter(is_traded, ComparisonOperator.EQUALS, true)) + .build() + val children = listOf(innerOr) + + val orExpression = OrExpression.newBuilder() + .addParam(RecordSubjectParameter(stock_count, ComparisonOperator.EQUALS, 10)) + .build() + + val result = OrExpression.newBuilder() + CartesianProducts.cartesianChildren(children, orExpression, result) + + val union = result.build() + union.children() shouldHaveSize 1 + val and = union.children()[0] as AndExpression + and.children() shouldHaveSize 1 + and.children()[0] shouldBe innerOr + } + } + + @Nested internal inner class + `multiply custom parameters over an 'OrExpression'` { + + @Test + fun `producing a union of conjunctions`() { + val column = object : CustomColumn() { + override fun name(): ColumnName = ColumnName.of("custom") + override fun type(): Class = String::class.java + override fun valueIn(source: Manufacturer): String = "value" + } + val param = CustomSubjectParameter(column, "value", ComparisonOperator.EQUALS) + val params = listOf(param) + + val orExpression = OrExpression.newBuilder() + .addParam(RecordSubjectParameter(stock_count, ComparisonOperator.EQUALS, 10)) + .addCustomParam(param) + .addExpression(AndExpression.newBuilder() + .addParam(RecordSubjectParameter(is_traded, ComparisonOperator.EQUALS, true)) + .build()) + .build() + + val result = OrExpression.newBuilder() + CartesianProducts.cartesianCustomParams(params, orExpression, result) + + val union = result.build() + union.children() shouldHaveSize 3 + } + } + + @Test + fun `have private constructor`() { + val constructor = CartesianProducts::class.java.getDeclaredConstructor() + constructor.isAccessible = true + val instance = constructor.newInstance() + instance shouldNotBe null + } +} diff --git a/base/src/test/kotlin/io/spine/query/EntityQuerySpec.kt b/base/src/test/kotlin/io/spine/query/EntityQuerySpec.kt new file mode 100644 index 0000000000..59c9cd5d78 --- /dev/null +++ b/base/src/test/kotlin/io/spine/query/EntityQuerySpec.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.query + +import com.google.common.testing.EqualsTester +import com.google.protobuf.Descriptors.Descriptor +import com.google.protobuf.Empty +import com.google.protobuf.FieldMask +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.spine.base.EntityState +import io.spine.testing.StubMessage +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`EntityQuery` should") +internal class EntityQuerySpec { + + private val secondsColumn = EntityColumn( + "seconds", Long::class.java, { 42L } + ) + + @Test + fun `be converted back to builder`() { + val builder = TestEntityQueryBuilder() + val query = builder.build() + query.toBuilder() shouldBe builder + } + + @Test + fun `be converted to 'RecordQuery'`() { + val builder = TestEntityQueryBuilder() + val criterion = EntityCriterion(secondsColumn, builder) + criterion.`is`(100L) + builder.withMask("seconds") + builder.sortAscendingBy(secondsColumn) + builder.limit(5) + + val query = builder.build() + val recordQuery = query.toRecordQuery() + + recordQuery shouldNotBe null + recordQuery.limit() shouldBe 5 + recordQuery.mask() shouldBe FieldMask.newBuilder().addPaths("seconds").build() + } + + @Test + fun `copy its state to another builder`() { + val builder = TestEntityQueryBuilder() + builder.sortAscendingBy(secondsColumn) + builder.limit(5) + val query = builder.build() + + val anotherBuilder = TestEntityQueryBuilder() + query.copyTo(anotherBuilder) + + val anotherQuery = anotherBuilder.build() + anotherQuery.limit() shouldBe 5 + } + + @Test + fun `support 'equals()' and 'hashCode()'`() { + val builder1 = TestEntityQueryBuilder() + val query1a = builder1.build() + val query1b = builder1.build() + + val builder2 = TestEntityQueryBuilder() + builder2.limit(10) + builder2.sortAscendingBy(secondsColumn) + val query2 = builder2.build() + + EqualsTester() + .addEqualityGroup(query1a, query1b) + .addEqualityGroup(query2) + .testEquals() + } +} + +/** + * A stub entity state for testing purposes. + */ +internal class StubState : StubMessage(), EntityState { + override fun getDescriptorForType(): Descriptor = com.google.protobuf.Timestamp.getDescriptor() + override fun getDefaultInstanceForType(): StubState = INSTANCE + companion object { + private val INSTANCE = StubState() + @JvmStatic + fun getDefaultInstance(): StubState = INSTANCE + } +} + +/** + * A concrete implementation of [EntityQuery] for testing purposes. + */ +private class TestEntityQuery(builder: TestEntityQueryBuilder) : + EntityQuery(builder) + +/** + * A concrete implementation of [EntityQueryBuilder] for testing purposes. + */ +private class TestEntityQueryBuilder : + EntityQueryBuilder( + String::class.java, StubState::class.java + ) { + override fun thisRef(): TestEntityQueryBuilder = this + override fun build(): TestEntityQuery = TestEntityQuery(this) +} From d99fc441342bef029a8721987207e7794c63681c Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 2 Jun 2026 22:48:39 +0100 Subject: [PATCH 21/45] Fix long lines --- .../src/test/kotlin/io/spine/query/CartesianProductsSpec.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/base/src/test/kotlin/io/spine/query/CartesianProductsSpec.kt b/base/src/test/kotlin/io/spine/query/CartesianProductsSpec.kt index 3c1c00658e..248bef3cdb 100644 --- a/base/src/test/kotlin/io/spine/query/CartesianProductsSpec.kt +++ b/base/src/test/kotlin/io/spine/query/CartesianProductsSpec.kt @@ -52,7 +52,8 @@ internal class CartesianProductsSpec { override fun type(): Class = String::class.java override fun valueIn(source: Manufacturer): String = "value" } - val customParam = CustomSubjectParameter(customColumn, "value", ComparisonOperator.EQUALS) + val customParam = + CustomSubjectParameter(customColumn, "value", ComparisonOperator.EQUALS) val orExpression = OrExpression.newBuilder() .addParam(RecordSubjectParameter(stock_count, ComparisonOperator.EQUALS, 10)) @@ -86,7 +87,8 @@ internal class CartesianProductsSpec { override fun type(): Class = String::class.java override fun valueIn(source: Manufacturer): String = "value" } - val customParam = CustomSubjectParameter(customColumn, "value", ComparisonOperator.EQUALS) + val customParam = + CustomSubjectParameter(customColumn, "value", ComparisonOperator.EQUALS) val orExpression = OrExpression.newBuilder() .addParam(RecordSubjectParameter(stock_count, ComparisonOperator.EQUALS, 10)) From fb88d708239b77149fbfeb2d869a98c40dce0bc9 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 2 Jun 2026 22:50:08 +0100 Subject: [PATCH 22/45] Remove unrelated task documents --- .../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 ------ 5 files changed, 986 deletions(-) delete mode 100644 .agents/tasks/buildsrc-gradle-review-findings.md delete mode 100644 .agents/tasks/cross-agent-skill-best-practices.md delete mode 100644 .agents/tasks/enforce-max-line-length.md delete mode 100644 .agents/tasks/gradle-caching-plan.md delete mode 100644 .agents/tasks/spine-task-group-constant.md diff --git a/.agents/tasks/buildsrc-gradle-review-findings.md b/.agents/tasks/buildsrc-gradle-review-findings.md deleted file mode 100644 index 8890e49419..0000000000 --- a/.agents/tasks/buildsrc-gradle-review-findings.md +++ /dev/null @@ -1,303 +0,0 @@ ---- -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 deleted file mode 100644 index 4876f28c86..0000000000 --- a/.agents/tasks/cross-agent-skill-best-practices.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -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 deleted file mode 100644 index 3cad1c6962..0000000000 --- a/.agents/tasks/enforce-max-line-length.md +++ /dev/null @@ -1,279 +0,0 @@ ---- -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 deleted file mode 100644 index efc3859ff2..0000000000 --- a/.agents/tasks/gradle-caching-plan.md +++ /dev/null @@ -1,200 +0,0 @@ ---- -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 deleted file mode 100644 index 24667819f9..0000000000 --- a/.agents/tasks/spine-task-group-constant.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -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]. From 53a05c8b14b1406bf23bae792660f41485506adc Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 2 Jun 2026 23:03:49 +0100 Subject: [PATCH 23/45] Add more tests Also: * Update task doc with progress * Add memory for testing utility classes --- .../memory/feedback/utility-class-testing.md | 38 +++++++ .agents/tasks/raise-base-coverage.md | 14 +-- .../java/io/spine/base/RejectionType.java | 3 + .../main/java/io/spine/base/UuidValue.java | 2 +- .../test/kotlin/io/spine/base/MistakeSpec.kt | 9 +- .../kotlin/io/spine/base/UuidValueSpec.kt | 34 ++++-- .../kotlin/io/spine/code/fs/FsObjectSpec.kt | 106 ++++++++++++++++++ base/src/test/kotlin/io/spine/io/FilesSpec.kt | 6 + base/src/test/kotlin/io/spine/io/PathsSpec.kt | 9 ++ .../io/spine/string/FnStringifierSpec.kt | 49 ++++++++ .../kotlin/io/spine/string/StringifySpec.kt | 63 +++++++++++ .../test/kotlin/io/spine/util/Math2Spec.kt | 68 +++++++++++ 12 files changed, 378 insertions(+), 23 deletions(-) create mode 100644 .agents/memory/feedback/utility-class-testing.md create mode 100644 base/src/test/kotlin/io/spine/code/fs/FsObjectSpec.kt create mode 100644 base/src/test/kotlin/io/spine/string/FnStringifierSpec.kt create mode 100644 base/src/test/kotlin/io/spine/string/StringifySpec.kt create mode 100644 base/src/test/kotlin/io/spine/util/Math2Spec.kt diff --git a/.agents/memory/feedback/utility-class-testing.md b/.agents/memory/feedback/utility-class-testing.md new file mode 100644 index 0000000000..2a16f4a254 --- /dev/null +++ b/.agents/memory/feedback/utility-class-testing.md @@ -0,0 +1,38 @@ +# Utility class testing + +In Spine libraries, utility classes (classes with only static methods and a private constructor) should be tested using `UtilityClassTest` as a base class. + +## Why + +`UtilityClassTest` automatically: +1. Verifies that the class is `final`. +2. Verifies that it has exactly one private constructor. +3. Verifies that the constructor throws an exception (usually `AssertionError`) or simply that it can be instantiated via reflection (depending on the implementation of `UtilityClassTest`), thus covering the private constructor for coverage purposes. + +## How + +### Kotlin + +Inherit from `UtilityClassTest(TargetClass::class.java)`: + +```kotlin +@DisplayName("`MyUtils` should") +internal class MyUtilsSpec : UtilityClassTest(MyUtils::class.java) { + // ... tests ... +} +``` + +### Java + +Inherit from `UtilityClassTest`: + +```java +@DisplayName("`MyUtils` should") +class MyUtilsTest extends UtilityClassTest { + + MyUtilsTest() { + super(MyUtils.class); + } + // ... tests ... +} +``` diff --git a/.agents/tasks/raise-base-coverage.md b/.agents/tasks/raise-base-coverage.md index 6e76be9605..cfc6176fa0 100644 --- a/.agents/tasks/raise-base-coverage.md +++ b/.agents/tasks/raise-base-coverage.md @@ -36,14 +36,14 @@ gaps closed without weakening Codecov settings. - [x] Read target sources and existing tests for selected gaps. - [x] Document proposed concrete test cases for approval. - [x] Remove deprecated API in the `:base` module. -- [ ] Analyze whether `io.spine.code.fs` is used in SpineEventEngine projects. -- [ ] Mark `io.spine.code.fs` types that are not used by any project as deprecated. -- [ ] Deprecate `io.spine.code.fs` types that are not used. -- [ ] Analyse whether `RejectionType` is used in Spine SDK projects and deprecate if not. -- [ ] Finalize test cases for the remaining non-deprecated API and wait for +- [x] Analyze whether `io.spine.code.fs` is used in SpineEventEngine projects. +- [x] Mark `io.spine.code.fs` types that are not used by any project as deprecated. +- [x] Deprecate `io.spine.code.fs` types that are not used. +- [x] Analyse whether `RejectionType` is used in Spine SDK projects and deprecate if not. +- [x] Finalize test cases for the remaining non-deprecated API and wait for approval to write tests. -- [ ] Add approved Kotlin `*Spec` tests using stubs, not mocks. -- [ ] Re-run `:base:koverXmlReport` and confirm targeted gaps closed. +- [x] Add approved Kotlin `*Spec` tests using stubs, not mocks. +- [x] Re-run `:base:koverXmlReport` and confirm targeted gaps closed. ## Updated Scope diff --git a/base/src/main/java/io/spine/base/RejectionType.java b/base/src/main/java/io/spine/base/RejectionType.java index 2ca3fe96e6..217755002f 100644 --- a/base/src/main/java/io/spine/base/RejectionType.java +++ b/base/src/main/java/io/spine/base/RejectionType.java @@ -40,7 +40,10 @@ /** * A code generation metadata on a rejection. + * + * @deprecated This type is no longer used and will be removed in the future. */ +@Deprecated(since = "2.0.0-SNAPSHOT.392", forRemoval = true) public final class RejectionType extends MessageType { /** diff --git a/base/src/main/java/io/spine/base/UuidValue.java b/base/src/main/java/io/spine/base/UuidValue.java index 6e8ffb2442..25debbaaad 100644 --- a/base/src/main/java/io/spine/base/UuidValue.java +++ b/base/src/main/java/io/spine/base/UuidValue.java @@ -69,7 +69,7 @@ static void checkValid(String uuid) { checkNotEmptyOrBlank(uuid); try { UUID.fromString(uuid); - } catch (NumberFormatException e) { + } catch (IllegalArgumentException e) { throw newIllegalArgumentException(e, "Invalid UUID string: `%s`.", uuid); } } diff --git a/base/src/test/kotlin/io/spine/base/MistakeSpec.kt b/base/src/test/kotlin/io/spine/base/MistakeSpec.kt index e4474c1668..f0960b71b5 100644 --- a/base/src/test/kotlin/io/spine/base/MistakeSpec.kt +++ b/base/src/test/kotlin/io/spine/base/MistakeSpec.kt @@ -70,10 +70,11 @@ internal class MistakeSpec { } } -private class KMistake(message: String?, cause: Throwable?) : Mistake(message, cause) { - constructor(message: String?) : this(message, null) - constructor(cause: Throwable?) : this(cause?.toString(), cause) - constructor() : this(null, null) +private class KMistake : Mistake { + constructor() : super() + constructor(message: String?) : super(message) + constructor(cause: Throwable?) : super(cause) + constructor(message: String?, cause: Throwable?) : super(message, cause) companion object { private const val serialVersionUID: Long = 0L diff --git a/base/src/test/kotlin/io/spine/base/UuidValueSpec.kt b/base/src/test/kotlin/io/spine/base/UuidValueSpec.kt index 1a68d803ea..778d66d1f9 100644 --- a/base/src/test/kotlin/io/spine/base/UuidValueSpec.kt +++ b/base/src/test/kotlin/io/spine/base/UuidValueSpec.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,25 +26,37 @@ package io.spine.base -import java.util.UUID +import io.kotest.assertions.throwables.shouldThrow import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertDoesNotThrow -import org.junit.jupiter.api.assertThrows +import java.util.UUID @DisplayName("`UuidValue` should") -internal class UuidValueSpec { +class UuidValueSpec { + + @Test + fun `validate UUID string`() { + UuidValue.checkValid(UUID.randomUUID().toString()) + } @Test - fun `provide validation method for a string value`() { - assertThrows { + fun `fail on empty string`() { + shouldThrow { UuidValue.checkValid("") } - assertThrows { - UuidValue.checkValid("1-2-3") + } + + @Test + fun `fail on blank string`() { + shouldThrow { + UuidValue.checkValid(" ") } - assertDoesNotThrow { - UuidValue.checkValid(UUID.randomUUID().toString()) + } + + @Test + fun `fail on invalid UUID string`() { + shouldThrow { + UuidValue.checkValid("not-a-uuid") } } } diff --git a/base/src/test/kotlin/io/spine/code/fs/FsObjectSpec.kt b/base/src/test/kotlin/io/spine/code/fs/FsObjectSpec.kt new file mode 100644 index 0000000000..0e3079cc8b --- /dev/null +++ b/base/src/test/kotlin/io/spine/code/fs/FsObjectSpec.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.code.fs + +import com.google.common.testing.EqualsTester +import io.kotest.matchers.shouldBe +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +@DisplayName("`FsObject` should") +class FsObjectSpec { + + private class StubFsObject(path: Path) : FsObject(path) + private class StubDir(path: Path) : AbstractDirectory(path) + private class StubSourceCodeDir(path: Path) : SourceCodeDirectory(path) + private class StubSourceFile(path: Path) : AbstractSourceFile(path) + + @Test + fun `expose path`(@TempDir tempDir: Path) { + val path = tempDir.resolve("some-file") + val obj = StubFsObject(path) + obj.path() shouldBe path + } + + @Test + fun `expose parent`(@TempDir tempDir: Path) { + val path = tempDir.resolve("parent/child") + val obj = StubFsObject(path) + obj.parent() shouldBe path.parent + } + + @Test + fun `tell if exists`(@TempDir tempDir: Path) { + val path = tempDir.resolve("real-file") + Files.createFile(path) + val obj = StubFsObject(path) + obj.exists() shouldBe true + + val missing = tempDir.resolve("missing-file") + StubFsObject(missing).exists() shouldBe false + } + + @Test + fun `provide 'toString'`() { + val path = Paths.get("some", "path") + val obj = StubFsObject(path) + obj.toString() shouldBe path.toString() + } + + @Test + fun `support equality`() { + val path1 = Paths.get("p1") + val path2 = Paths.get("p2") + EqualsTester() + .addEqualityGroup(StubFsObject(path1), StubFsObject(path1)) + .addEqualityGroup(StubFsObject(path2)) + .testEquals() + } + + @Nested internal inner class + `Source code directory` { + + @Test + fun `resolve child directory`() { + val root = StubSourceCodeDir(Paths.get("root")) + val child = StubSourceCodeDir(Paths.get("child")) + root.resolve(child) shouldBe Paths.get("root", "child") + } + + @Test + fun `resolve source file`() { + val root = StubSourceCodeDir(Paths.get("root")) + val file = StubSourceFile(Paths.get("File.java")) + root.resolve(file) shouldBe Paths.get("root", "File.java") + } + } +} diff --git a/base/src/test/kotlin/io/spine/io/FilesSpec.kt b/base/src/test/kotlin/io/spine/io/FilesSpec.kt index b24bd73a3f..5fa1b749c0 100644 --- a/base/src/test/kotlin/io/spine/io/FilesSpec.kt +++ b/base/src/test/kotlin/io/spine/io/FilesSpec.kt @@ -45,4 +45,10 @@ internal class FilesSpec { File("file.txt").replaceExtension("") shouldBe File("file") File("file.").replaceExtension("") shouldBe File("file") } + + @Test + fun `convert to Unix-style path`() { + File("my\\windows\\path").toUnixPath() shouldBe "my/windows/path" + File("my/unix/path").toUnixPath() shouldBe "my/unix/path" + } } diff --git a/base/src/test/kotlin/io/spine/io/PathsSpec.kt b/base/src/test/kotlin/io/spine/io/PathsSpec.kt index cb115e3f66..b3dd79a13e 100644 --- a/base/src/test/kotlin/io/spine/io/PathsSpec.kt +++ b/base/src/test/kotlin/io/spine/io/PathsSpec.kt @@ -34,6 +34,7 @@ import java.io.File import java.nio.file.Paths import kotlin.io.path.Path import kotlin.io.path.div +import kotlin.io.path.pathString import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -71,4 +72,12 @@ internal class PathsSpec { val path = "/my/unix/path" path.toUnix() shouldBeSameInstanceAs path } + + @Test + fun `convert 'Path' to Unix separators`() { + Path("my\\windows\\path").toUnix().pathString shouldBe "my/windows/path" + + val unixPath = Path("my/unix/path") + unixPath.toUnix() shouldBeSameInstanceAs unixPath + } } diff --git a/base/src/test/kotlin/io/spine/string/FnStringifierSpec.kt b/base/src/test/kotlin/io/spine/string/FnStringifierSpec.kt new file mode 100644 index 0000000000..d488f5645e --- /dev/null +++ b/base/src/test/kotlin/io/spine/string/FnStringifierSpec.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.string + +import io.kotest.matchers.shouldBe +import io.spine.util.SerializableFunction +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`FnStringifier` should") +class FnStringifierSpec { + + private class StubStringifier : FnStringifier( + "stub", + SerializableFunction { it.toString() }, + SerializableFunction { it.toInt() } + ) + + @Test + fun `convert using functions`() { + val stringifier = StubStringifier() + stringifier.convert(10) shouldBe "10" + stringifier.reverse().convert("20") shouldBe 20 + } +} diff --git a/base/src/test/kotlin/io/spine/string/StringifySpec.kt b/base/src/test/kotlin/io/spine/string/StringifySpec.kt new file mode 100644 index 0000000000..1978e992a4 --- /dev/null +++ b/base/src/test/kotlin/io/spine/string/StringifySpec.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.string + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("Top-level functions in `Stringify.kt` should") +class StringifySpec { + + @Test + fun `stringify an object`() { + val value = 123 + value.stringify() shouldBe "123" + } + + @Test + fun `parse from string using type parameter`() { + fromString("123") shouldBe 123 + } + + @Test + fun `parse from string using 'KClass'`() { + fromString("123", java.lang.Integer::class) shouldBe 123 + } + + @Test + fun `create list stringifier`() { + listStringifier().convert(listOf(1, 2)) shouldBe "\"1\",\"2\"" + listStringifier('#').convert(listOf(1, 2)) shouldBe "\"1\"#\"2\"" + } + + @Test + fun `create map stringifier`() { + mapStringifier().convert(mapOf(1 to "a")) shouldBe "\"1\":\"a\"" + mapStringifier('#').convert(mapOf(1 to "a", 2 to "b")) shouldBe "\"1\":\"a\"#\"2\":\"b\"" + } +} diff --git a/base/src/test/kotlin/io/spine/util/Math2Spec.kt b/base/src/test/kotlin/io/spine/util/Math2Spec.kt new file mode 100644 index 0000000000..6066aaeb6d --- /dev/null +++ b/base/src/test/kotlin/io/spine/util/Math2Spec.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.util + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.spine.testing.UtilityClassTest +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`Math2` should") +class Math2Spec : UtilityClassTest(Math2::class.java) { + + @Test + fun `multiply long by int`() { + Math2.safeMultiply(10L, 2) shouldBe 20L + Math2.safeMultiply(10L, 0) shouldBe 0L + Math2.safeMultiply(10L, 1) shouldBe 10L + Math2.safeMultiply(10L, -1) shouldBe -10L + Math2.safeMultiply(Long.MAX_VALUE, 1) shouldBe Long.MAX_VALUE + } + + @Test + fun `fail to multiply on overflow`() { + shouldThrow { + Math2.safeMultiply(Long.MAX_VALUE, 2) + } + shouldThrow { + Math2.safeMultiply(Long.MIN_VALUE, -1) + } + } + + @Test + fun `perform floor division`() { + Math2.floorDiv(0, 4) shouldBe 0L + Math2.floorDiv(-1, 4) shouldBe -1L + Math2.floorDiv(-2, 4) shouldBe -1L + Math2.floorDiv(-3, 4) shouldBe -1L + Math2.floorDiv(-4, 4) shouldBe -1L + Math2.floorDiv(-5, 4) shouldBe -2L + Math2.floorDiv(4, 4) shouldBe 1L + Math2.floorDiv(5, 4) shouldBe 1L + } +} From 362244e3417a688f317b10d69541d0cafdb3aa0a Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 2 Jun 2026 23:05:12 +0100 Subject: [PATCH 24/45] Address warnings --- base/src/test/kotlin/io/spine/code/fs/FsObjectSpec.kt | 1 - base/src/test/kotlin/io/spine/string/StringifySpec.kt | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/base/src/test/kotlin/io/spine/code/fs/FsObjectSpec.kt b/base/src/test/kotlin/io/spine/code/fs/FsObjectSpec.kt index 0e3079cc8b..2ecdfb2826 100644 --- a/base/src/test/kotlin/io/spine/code/fs/FsObjectSpec.kt +++ b/base/src/test/kotlin/io/spine/code/fs/FsObjectSpec.kt @@ -40,7 +40,6 @@ import org.junit.jupiter.api.io.TempDir class FsObjectSpec { private class StubFsObject(path: Path) : FsObject(path) - private class StubDir(path: Path) : AbstractDirectory(path) private class StubSourceCodeDir(path: Path) : SourceCodeDirectory(path) private class StubSourceFile(path: Path) : AbstractSourceFile(path) diff --git a/base/src/test/kotlin/io/spine/string/StringifySpec.kt b/base/src/test/kotlin/io/spine/string/StringifySpec.kt index 1978e992a4..6529f44ae5 100644 --- a/base/src/test/kotlin/io/spine/string/StringifySpec.kt +++ b/base/src/test/kotlin/io/spine/string/StringifySpec.kt @@ -45,6 +45,8 @@ class StringifySpec { } @Test + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "RemoveRedundantQualifierName") + // To use `java.lang.Integer` instead of `kotlin.Int` for testing. fun `parse from string using 'KClass'`() { fromString("123", java.lang.Integer::class) shouldBe 123 } @@ -58,6 +60,7 @@ class StringifySpec { @Test fun `create map stringifier`() { mapStringifier().convert(mapOf(1 to "a")) shouldBe "\"1\":\"a\"" - mapStringifier('#').convert(mapOf(1 to "a", 2 to "b")) shouldBe "\"1\":\"a\"#\"2\":\"b\"" + mapStringifier('#').convert(mapOf(1 to "a", 2 to "b")) shouldBe + "\"1\":\"a\"#\"2\":\"b\"" } } From 25e8cfb3ec7ce7718d14225ca8f47beedba386db Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 3 Jun 2026 01:09:48 +0300 Subject: [PATCH 25/45] Fix typo in the test suite name Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- base/src/test/kotlin/io/spine/string/StringifiersSpec.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/base/src/test/kotlin/io/spine/string/StringifiersSpec.kt b/base/src/test/kotlin/io/spine/string/StringifiersSpec.kt index 2ef53e7ad5..1aeb58dae8 100644 --- a/base/src/test/kotlin/io/spine/string/StringifiersSpec.kt +++ b/base/src/test/kotlin/io/spine/string/StringifiersSpec.kt @@ -116,8 +116,7 @@ internal class StringifiersSpec : UtilityClassTest(Stringifiers::c } @Nested internal inner class - `create 'Stringifier' with a delimeter for` { - + `create 'Stringifier' with a delimiter for` { @Test fun List() { val stamps: List = createList() From c02e1db84d71037f8eee80864faec7707cf52b9a Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 3 Jun 2026 01:10:37 +0300 Subject: [PATCH 26/45] Update the reference to guideline document Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/project.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/project.md b/docs/project.md index 9e415d8a45..a198b69d38 100644 --- a/docs/project.md +++ b/docs/project.md @@ -27,8 +27,8 @@ Key constraints: - Public API stability matters: downstream Spine repos pin to versions published from here, so removals and signature changes are breaking. - No analytics, telemetry, reflection, or unsafe code (see - `.agents/safety-rules.md`). -- Versioning follows the Spine SDK policy (`.agents/version-policy.md`); + `.agents/guidelines/safety-rules.md`). +- Versioning follows the Spine SDK policy (`.agents/guidelines/version-policy.md`); CI's `Version Guard` rejects branches that reuse a published version. - Dependency declarations live under `buildSrc/src/main/kotlin/io/spine/dependency/` and are audited by the From b3b1584330845f42089f48946f7396345935aadd Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 3 Jun 2026 01:10:56 +0300 Subject: [PATCH 27/45] Update (c) year Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- base/src/test/kotlin/io/spine/protobuf/DiffKtSpec.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/src/test/kotlin/io/spine/protobuf/DiffKtSpec.kt b/base/src/test/kotlin/io/spine/protobuf/DiffKtSpec.kt index f152ee63dc..c55276c6e1 100644 --- a/base/src/test/kotlin/io/spine/protobuf/DiffKtSpec.kt +++ b/base/src/test/kotlin/io/spine/protobuf/DiffKtSpec.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. From 90780f4d0de1d80243a29c8c5747e225b0b0eae0 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 3 Jun 2026 01:12:24 +0300 Subject: [PATCH 28/45] Update (c) year Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- base/src/test/kotlin/io/spine/code/proto/TypeSetKtSpec.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/src/test/kotlin/io/spine/code/proto/TypeSetKtSpec.kt b/base/src/test/kotlin/io/spine/code/proto/TypeSetKtSpec.kt index e1d27028ef..7e55db872f 100644 --- a/base/src/test/kotlin/io/spine/code/proto/TypeSetKtSpec.kt +++ b/base/src/test/kotlin/io/spine/code/proto/TypeSetKtSpec.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. From f1c8690b3bbe84acc3446d88c12eade98b0b0201 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 3 Jun 2026 01:12:43 +0300 Subject: [PATCH 29/45] Update (c) year Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- base/src/test/kotlin/io/spine/code/proto/FieldContextKtSpec.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/src/test/kotlin/io/spine/code/proto/FieldContextKtSpec.kt b/base/src/test/kotlin/io/spine/code/proto/FieldContextKtSpec.kt index aa664935cb..d46195c494 100644 --- a/base/src/test/kotlin/io/spine/code/proto/FieldContextKtSpec.kt +++ b/base/src/test/kotlin/io/spine/code/proto/FieldContextKtSpec.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. From 37160d68f446d7b504584af5f69eeab4c993237a Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 3 Jun 2026 01:13:00 +0300 Subject: [PATCH 30/45] Update (c) year Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../test/kotlin/io/spine/code/fs/AbstractSourceFileKtSpec.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/src/test/kotlin/io/spine/code/fs/AbstractSourceFileKtSpec.kt b/base/src/test/kotlin/io/spine/code/fs/AbstractSourceFileKtSpec.kt index 0d93c5ecdd..0962df5dda 100644 --- a/base/src/test/kotlin/io/spine/code/fs/AbstractSourceFileKtSpec.kt +++ b/base/src/test/kotlin/io/spine/code/fs/AbstractSourceFileKtSpec.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. From 4e1c0206b4cdec12174615ecdd98f244532ef7e4 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 3 Jun 2026 01:13:15 +0300 Subject: [PATCH 31/45] Update (c) year Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- base/src/test/kotlin/io/spine/base/RejectionTypeKtSpec.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/src/test/kotlin/io/spine/base/RejectionTypeKtSpec.kt b/base/src/test/kotlin/io/spine/base/RejectionTypeKtSpec.kt index 04da4e1e29..48e1585b8d 100644 --- a/base/src/test/kotlin/io/spine/base/RejectionTypeKtSpec.kt +++ b/base/src/test/kotlin/io/spine/base/RejectionTypeKtSpec.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. From b938540c3f52807e7597ae68fc7a7cd8094851e2 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 2 Jun 2026 23:17:31 +0100 Subject: [PATCH 32/45] Instruct agents to use `EqualsTester` for testing `equals()` and `hashCode()` --- .agents/memory/feedback/equals-tester.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .agents/memory/feedback/equals-tester.md diff --git a/.agents/memory/feedback/equals-tester.md b/.agents/memory/feedback/equals-tester.md new file mode 100644 index 0000000000..39725878c2 --- /dev/null +++ b/.agents/memory/feedback/equals-tester.md @@ -0,0 +1,24 @@ +# Testing `equals()` and `hashCode()` + +## Use `EqualsTester` + +When testing the implementation of `equals()` and `hashCode()` in Java or Kotlin classes, always use Guava's `EqualsTester`. + +### Correct example: +```kotlin +import com.google.common.testing.EqualsTester + +// ... inside a test method +EqualsTester() + .addEqualityGroup(obj1A, obj1B) + .addEqualityGroup(obj2) + .testEquals() +``` + +### Why: +`EqualsTester` automatically verifies: +1. Symmetry: `a.equals(b) == b.equals(a)` +2. Transitivity: `a.equals(b) && b.equals(c) => a.equals(c)` +3. Reflexivity: `a.equals(a)` +4. Inequality with `null`. +5. Consistency between `equals()` and `hashCode()`. From f175cdd6b7afe415141fdfff35f6decdd186286f Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 2 Jun 2026 23:17:46 +0100 Subject: [PATCH 33/45] Update build time --- docs/dependencies/dependencies.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md index 5c37a7951c..b96dfe0a54 100644 --- a/docs/dependencies/dependencies.md +++ b/docs/dependencies/dependencies.md @@ -764,7 +764,7 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 02 22:17:18 WEST 2026** using +This report was generated on **Tue Jun 02 23:05:26 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). @@ -1616,7 +1616,7 @@ This report was generated on **Tue Jun 02 22:17:18 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 02 22:17:18 WEST 2026** using +This report was generated on **Tue Jun 02 23:05:26 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). @@ -2446,7 +2446,7 @@ This report was generated on **Tue Jun 02 22:17:18 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 02 22:17:18 WEST 2026** using +This report was generated on **Tue Jun 02 23:05:26 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). @@ -3356,6 +3356,6 @@ This report was generated on **Tue Jun 02 22:17:18 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 02 22:17:18 WEST 2026** using +This report was generated on **Tue Jun 02 23:05:26 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 From 48b874be6b09ad41e7dbe721bb05ae372844b659 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 3 Jun 2026 01:18:21 +0300 Subject: [PATCH 34/45] Remove trailing whitespaces Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- base/src/test/kotlin/io/spine/query/EntityQuerySpec.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base/src/test/kotlin/io/spine/query/EntityQuerySpec.kt b/base/src/test/kotlin/io/spine/query/EntityQuerySpec.kt index 59c9cd5d78..bdad4dec68 100644 --- a/base/src/test/kotlin/io/spine/query/EntityQuerySpec.kt +++ b/base/src/test/kotlin/io/spine/query/EntityQuerySpec.kt @@ -87,12 +87,12 @@ internal class EntityQuerySpec { val builder1 = TestEntityQueryBuilder() val query1a = builder1.build() val query1b = builder1.build() - + val builder2 = TestEntityQueryBuilder() builder2.limit(10) builder2.sortAscendingBy(secondsColumn) val query2 = builder2.build() - + EqualsTester() .addEqualityGroup(query1a, query1b) .addEqualityGroup(query2) From 032b1a370d4589212612d14e02276130dd7e73f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:11:52 +0000 Subject: [PATCH 35/45] Use reference-style link for core-jvm in project docs --- docs/project.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/project.md b/docs/project.md index a198b69d38..ce065f479a 100644 --- a/docs/project.md +++ b/docs/project.md @@ -5,7 +5,7 @@ `base-libraries` is a foundational JVM repository in the Spine SDK organisation. It hosts the common data types, annotations, environment helpers, and parsing/serialization utilities that the rest of the Spine SDK (notably -[`core-jvm`](https://github.com/SpineEventEngine/core-java)) depends on. The +[`core-jvm`][core-jvm]) depends on. The artifacts published from this repo sit at the bottom of the Spine dependency graph, so changes here ripple into most other Spine projects. @@ -36,3 +36,5 @@ Key constraints: Read [`.agents/jvm-project.md`](jvm-project.md) for build stack, coding style, tests, and versioning. + +[core-jvm]: https://github.com/SpineEventEngine/core-java From ee6f21700077563268b18c8342cc61d8586e2c89 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 3 Jun 2026 19:13:57 +0300 Subject: [PATCH 36/45] Use reference-style link Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/project.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/project.md b/docs/project.md index ce065f479a..fd36a62bed 100644 --- a/docs/project.md +++ b/docs/project.md @@ -9,6 +9,7 @@ parsing/serialization utilities that the rest of the Spine SDK (notably artifacts published from this repo sit at the bottom of the Spine dependency graph, so changes here ripple into most other Spine projects. +[core-jvm]: https://github.com/SpineEventEngine/core-java ## Architecture Role: **library** (multi-module Gradle build) publishing the following Maven From 551a9ae2a5acac40c7e29dd2ec67f4d51c5818cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:12:16 +0000 Subject: [PATCH 37/45] Bump snapshot version to next breaking-change step --- version.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle.kts b/version.gradle.kts index cce8164f08..3047396fd9 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -24,4 +24,4 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -val versionToPublish: String by extra("2.0.0-SNAPSHOT.392") +val versionToPublish: String by extra("2.0.0-SNAPSHOT.400") From 7f5ce6d15cbf8b0ab3dfa8c04cfe418f93e8cd27 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:16:48 +0000 Subject: [PATCH 38/45] Move core-jvm link reference to document bottom --- docs/project.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/project.md b/docs/project.md index fd36a62bed..ce065f479a 100644 --- a/docs/project.md +++ b/docs/project.md @@ -9,7 +9,6 @@ parsing/serialization utilities that the rest of the Spine SDK (notably artifacts published from this repo sit at the bottom of the Spine dependency graph, so changes here ripple into most other Spine projects. -[core-jvm]: https://github.com/SpineEventEngine/core-java ## Architecture Role: **library** (multi-module Gradle build) publishing the following Maven From fe496824dc3805ccbe8c0357b292b849b0156746 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 3 Jun 2026 19:16:57 +0300 Subject: [PATCH 39/45] Optimise imports Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- base/src/test/kotlin/io/spine/query/EntityQuerySpec.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/base/src/test/kotlin/io/spine/query/EntityQuerySpec.kt b/base/src/test/kotlin/io/spine/query/EntityQuerySpec.kt index bdad4dec68..7b16677e71 100644 --- a/base/src/test/kotlin/io/spine/query/EntityQuerySpec.kt +++ b/base/src/test/kotlin/io/spine/query/EntityQuerySpec.kt @@ -28,7 +28,6 @@ package io.spine.query import com.google.common.testing.EqualsTester import com.google.protobuf.Descriptors.Descriptor -import com.google.protobuf.Empty import com.google.protobuf.FieldMask import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe From 782f8907db44a4ba22b0cdc1f1db876051547b40 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 3 Jun 2026 19:18:20 +0300 Subject: [PATCH 40/45] Fix guideline link Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/project.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/project.md b/docs/project.md index ce065f479a..42865298d2 100644 --- a/docs/project.md +++ b/docs/project.md @@ -34,7 +34,7 @@ Key constraints: `buildSrc/src/main/kotlin/io/spine/dependency/` and are audited by the `dependency-audit` skill. -Read [`.agents/jvm-project.md`](jvm-project.md) for build stack, coding +Read [`.agents/guidelines/jvm-project.md`](../.agents/guidelines/jvm-project.md) for build stack, coding style, tests, and versioning. [core-jvm]: https://github.com/SpineEventEngine/core-java From d96e5fdfbf77f785fa5683ce52b9f8769added24 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 3 Jun 2026 19:19:11 +0300 Subject: [PATCH 41/45] Optimise imports Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- base/src/test/kotlin/io/spine/code/proto/FileSetSpec.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/base/src/test/kotlin/io/spine/code/proto/FileSetSpec.kt b/base/src/test/kotlin/io/spine/code/proto/FileSetSpec.kt index f5d88182e5..56515f6a0f 100644 --- a/base/src/test/kotlin/io/spine/code/proto/FileSetSpec.kt +++ b/base/src/test/kotlin/io/spine/code/proto/FileSetSpec.kt @@ -27,14 +27,10 @@ package io.spine.code.proto import com.google.common.testing.EqualsTester -import com.google.protobuf.DescriptorProtos.FileDescriptorProto import com.google.protobuf.Empty -import io.kotest.matchers.collections.shouldContainExactly -import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.ints.shouldBeLessThan import io.kotest.matchers.optional.shouldBePresent import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe import io.kotest.matchers.string.shouldContain import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName From 2551d45a14592c34e7d3c47b02f1ac5cc71a187f Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Wed, 3 Jun 2026 17:28:53 +0100 Subject: [PATCH 42/45] Configure Git for Windows symlink compatibility --- .github/workflows/build-on-windows.yml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-on-windows.yml b/.github/workflows/build-on-windows.yml index 91a0bfef32..a4c445438f 100644 --- a/.github/workflows/build-on-windows.yml +++ b/.github/workflows/build-on-windows.yml @@ -8,9 +8,24 @@ jobs: name: Build on Windows steps: + - name: Configure Git for Windows symlink compatibility + shell: pwsh + run: | + # Avoid creating/expecting native symlinks on Windows runners. + # This helps when repository paths (like .agents/guidelines) are represented + # via links that can trigger EPERM on stat/access. + git config --global core.symlinks false + - uses: actions/checkout@v4 with: - submodules: 'true' + submodules: recursive + fetch-depth: 0 + + - name: Sync and update submodules (recursive) + shell: pwsh + run: | + git submodule sync --recursive + git submodule update --init --recursive - uses: actions/setup-java@v4 with: @@ -33,4 +48,4 @@ jobs: if: always() # always run even if the previous step fails with: report_paths: '**/build/test-results/**/TEST-*.xml' - require_tests: true # will fail workflow if test reports not found + require_tests: false # don't mask the primary failure if build aborts early From 79b959dffb5124662d1679937baec718b8b3d297 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Wed, 3 Jun 2026 17:47:02 +0100 Subject: [PATCH 43/45] Deprecate `Path.toUnix()` as not needed --- base/src/main/kotlin/io/spine/io/Paths.kt | 5 +++++ base/src/test/kotlin/io/spine/io/PathsSpec.kt | 8 -------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/base/src/main/kotlin/io/spine/io/Paths.kt b/base/src/main/kotlin/io/spine/io/Paths.kt index 971ffb8a1b..4c1f54d5bc 100644 --- a/base/src/main/kotlin/io/spine/io/Paths.kt +++ b/base/src/main/kotlin/io/spine/io/Paths.kt @@ -62,6 +62,11 @@ public fun Path.replaceExtension(newExtension: String): Path { * @return `this` if the path is already delimited as required, otherwise creates * a new instance with [Windows][Separator.Windows] file separators replaced. */ +@Deprecated( + message = "This function is not needed." + + " The `Path` API handles separators correctly across platforms.", + replaceWith = ReplaceWith("this") +) public fun Path.toUnix(): Path = if (pathString.contains(Separator.Windows)) { Path(pathString.toUnix()) diff --git a/base/src/test/kotlin/io/spine/io/PathsSpec.kt b/base/src/test/kotlin/io/spine/io/PathsSpec.kt index b3dd79a13e..c9a4b57586 100644 --- a/base/src/test/kotlin/io/spine/io/PathsSpec.kt +++ b/base/src/test/kotlin/io/spine/io/PathsSpec.kt @@ -72,12 +72,4 @@ internal class PathsSpec { val path = "/my/unix/path" path.toUnix() shouldBeSameInstanceAs path } - - @Test - fun `convert 'Path' to Unix separators`() { - Path("my\\windows\\path").toUnix().pathString shouldBe "my/windows/path" - - val unixPath = Path("my/unix/path") - unixPath.toUnix() shouldBeSameInstanceAs unixPath - } } From d142e378618c07c852b3a0e00de60e576d0a3821 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Wed, 3 Jun 2026 17:47:58 +0100 Subject: [PATCH 44/45] Update dependency reports --- docs/dependencies/dependencies.md | 16 ++++++++-------- docs/dependencies/pom.xml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md index b96dfe0a54..c6248653f6 100644 --- a/docs/dependencies/dependencies.md +++ b/docs/dependencies/dependencies.md @@ -1,6 +1,6 @@ -# Dependencies of `io.spine:spine-annotations:2.0.0-SNAPSHOT.392` +# Dependencies of `io.spine:spine-annotations:2.0.0-SNAPSHOT.400` ## Runtime 1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.0.2. @@ -764,14 +764,14 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 02 23:05:26 WEST 2026** using +This report was generated on **Wed Jun 03 17:47:35 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-base:2.0.0-SNAPSHOT.392` +# Dependencies of `io.spine:spine-base:2.0.0-SNAPSHOT.400` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -1616,14 +1616,14 @@ This report was generated on **Tue Jun 02 23:05:26 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 02 23:05:26 WEST 2026** using +This report was generated on **Wed Jun 03 17:47:35 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-environment:2.0.0-SNAPSHOT.392` +# Dependencies of `io.spine:spine-environment:2.0.0-SNAPSHOT.400` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -2446,14 +2446,14 @@ This report was generated on **Tue Jun 02 23:05:26 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 02 23:05:26 WEST 2026** using +This report was generated on **Wed Jun 03 17:47:35 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-format:2.0.0-SNAPSHOT.392` +# Dependencies of `io.spine:spine-format:2.0.0-SNAPSHOT.400` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.21.3. @@ -3356,6 +3356,6 @@ This report was generated on **Tue Jun 02 23:05:26 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 02 23:05:26 WEST 2026** using +This report was generated on **Wed Jun 03 17:47:35 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 f7f612abab..93f88ef80f 100644 --- a/docs/dependencies/pom.xml +++ b/docs/dependencies/pom.xml @@ -10,7 +10,7 @@ all modules and does not describe the project structure per-subproject. --> io.spine base-libraries -2.0.0-SNAPSHOT.392 +2.0.0-SNAPSHOT.400 2015 From 999213528b22a792f25e8520b85924b58141155a Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Wed, 3 Jun 2026 18:04:28 +0100 Subject: [PATCH 45/45] Remove duplicated step, resture `require_tests` --- .github/workflows/build-on-windows.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/build-on-windows.yml b/.github/workflows/build-on-windows.yml index a4c445438f..c4ba000bee 100644 --- a/.github/workflows/build-on-windows.yml +++ b/.github/workflows/build-on-windows.yml @@ -21,12 +21,6 @@ jobs: submodules: recursive fetch-depth: 0 - - name: Sync and update submodules (recursive) - shell: pwsh - run: | - git submodule sync --recursive - git submodule update --init --recursive - - uses: actions/setup-java@v4 with: java-version: 17 @@ -48,4 +42,4 @@ jobs: if: always() # always run even if the previous step fails with: report_paths: '**/build/test-results/**/TEST-*.xml' - require_tests: false # don't mask the primary failure if build aborts early + require_tests: true