From fd7222ed9c82a09a7aee7e843a4e4b764d8cb1c8 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Tue, 30 Jun 2026 00:24:48 +0100 Subject: [PATCH 01/12] Bump version -> `2.0.0-SNAPSHOT.243` --- version.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle.kts b/version.gradle.kts index bd2872a06..f494141ff 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -27,4 +27,4 @@ /** * The version of this library for publishing. */ -val versionToPublish by extra("2.0.0-SNAPSHOT.242") +val versionToPublish by extra("2.0.0-SNAPSHOT.243") From 8c9755f309a6b5a36d87180a435c5a8226e4148a Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Tue, 30 Jun 2026 00:26:55 +0100 Subject: [PATCH 02/12] Sync shared project configuration Run './config/pull' to bring the 'config' submodule to the tip of 'origin/master' and propagate the updated shared configuration (skills, guidelines, workflows, buildSrc, Gradle wrapper) into the repo. --- .agents/_TOC.md | 23 -- .agents/advanced-safety-rules.md | 6 - .agents/coding-guidelines.md | 39 -- .agents/common-tasks.md | 6 - .agents/documentation-guidelines.md | 14 - .agents/documentation-tasks.md | 20 - .agents/guidelines | 1 + .agents/project-structure-expectations.md | 21 - .agents/project.md | 1 + .agents/quick-reference-card.md | 9 - .agents/refactoring-guidelines.md | 3 - .agents/running-builds.md | 18 - .agents/safety-rules.md | 7 - .agents/scripts | 1 + .agents/shared | 1 + .agents/skills | 1 + .agents/skills/bump-gradle/SKILL.md | 117 ------ .agents/skills/bump-gradle/agents/openai.yaml | 4 - .agents/skills/bump-version/SKILL.md | 118 ------ .../skills/bump-version/agents/openai.yaml | 4 - .agents/skills/dependency-audit/SKILL.md | 115 ------ .agents/skills/dependency-update/SKILL.md | 278 ------------- .../dependency-update/agents/openai.yaml | 4 - .agents/skills/java-to-kotlin/SKILL.md | 51 --- .../skills/java-to-kotlin/agents/openai.yaml | 4 - .agents/skills/kotlin-review/SKILL.md | 62 --- .agents/skills/move-files/SKILL.md | 49 --- .agents/skills/move-files/agents/openai.yaml | 4 - .agents/skills/pre-pr/SKILL.md | 173 -------- .agents/skills/review-docs/SKILL.md | 129 ------ .agents/skills/update-copyright/SKILL.md | 16 - .../update-copyright/agents/openai.yaml | 4 - .../scripts/update_copyright.py | 389 ----------------- .../tests/test_update_copyright.py | 130 ------ .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 | 15 - .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/review-docs.md | 21 - .claude/commands/run-build.md | 12 - .claude/commands/update-copyright.md | 12 - .claude/commands/write-docs.md | 14 - .claude/settings.json | 28 +- .codecov.yml | 6 +- .github/copilot-instructions.md | 45 ++ .github/workflows/build-on-ubuntu.yml | 70 +++- .github/workflows/build-on-windows.yml | 15 +- .github/workflows/check-links.yml | 216 ++++++++++ .github/workflows/ensure-reports-updated.yml | 16 +- .../workflows/gradle-wrapper-validation.yml | 2 +- .github/workflows/increment-guard.yml | 89 +++- .github/workflows/publish.yml | 22 +- ...move-obsolete-artifacts-from-packages.yaml | 2 +- .github/workflows/revalidate-versions.yml | 57 +++ .github/workflows/secret-scan.yml | 70 ++++ .gitignore | 88 +++- .gitmodules | 6 + .idea/live-templates/README.md | 6 +- .idea/live-templates/User.xml | 2 +- .idea/misc.xml | 17 +- .junie/guidelines.md | 2 +- AGENTS.md | 134 +++++- CLAUDE.md | 71 +--- buildSrc/build.gradle.kts | 12 +- buildSrc/quality/pmd.xml | 4 +- buildSrc/src/main/kotlin/BuildExtensions.kt | 19 +- .../src/main/kotlin/DependencyResolution.kt | 6 +- buildSrc/src/main/kotlin/DokkaExts.kt | 7 +- buildSrc/src/main/kotlin/LicenseSettings.kt | 50 +-- buildSrc/src/main/kotlin/Strings.kt | 4 +- .../src/main/kotlin/config-tester.gradle.kts | 9 +- .../src/main/kotlin/dokka-setup.gradle.kts | 11 +- .../kotlin/io/spine/dependency/Dependency.kt | 16 +- .../io/spine/dependency/boms/BomsPlugin.kt | 9 +- .../spine/dependency/build/AnimalSniffer.kt | 4 +- .../dependency/build/CheckerFramework.kt | 4 +- .../io/spine/dependency/build/ErrorProne.kt | 4 +- .../io/spine/dependency/build/GradleDoctor.kt | 4 +- .../kotlin/io/spine/dependency/build/Ksp.kt | 4 +- .../dependency/build/PluginPublishPlugin.kt | 4 +- .../kotlin/io/spine/dependency/build/Pmd.kt | 4 +- .../io/spine/dependency/kotlinx/AtomicFu.kt | 2 +- .../io/spine/dependency/kotlinx/Coroutines.kt | 2 +- .../io/spine/dependency/kotlinx/DateTime.kt | 4 +- .../spine/dependency/kotlinx/Serialization.kt | 2 +- .../kotlin/io/spine/dependency/lib/Aedile.kt | 4 +- .../kotlin/io/spine/dependency/lib/Asm.kt | 4 +- .../kotlin/io/spine/dependency/lib/Auto.kt | 4 +- .../io/spine/dependency/lib/Caffeine.kt | 4 +- .../kotlin/io/spine/dependency/lib/Clikt.kt | 4 +- .../io/spine/dependency/lib/CommonsCli.kt | 6 +- .../io/spine/dependency/lib/CommonsCodec.kt | 4 +- .../io/spine/dependency/lib/CommonsLogging.kt | 4 +- .../io/spine/dependency/lib/Firebase.kt | 4 +- .../kotlin/io/spine/dependency/lib/Flogger.kt | 4 +- .../io/spine/dependency/lib/GoogleApis.kt | 10 +- .../io/spine/dependency/lib/GoogleCloud.kt | 21 +- .../kotlin/io/spine/dependency/lib/Grpc.kt | 4 +- .../io/spine/dependency/lib/GrpcKotlin.kt | 4 +- .../kotlin/io/spine/dependency/lib/Gson.kt | 4 +- .../kotlin/io/spine/dependency/lib/Guava.kt | 4 +- .../io/spine/dependency/lib/HttpClient.kt | 4 +- .../io/spine/dependency/lib/IntelliJ.kt | 6 +- .../kotlin/io/spine/dependency/lib/J2ObjC.kt | 4 +- .../kotlin/io/spine/dependency/lib/Jackson.kt | 6 +- .../io/spine/dependency/lib/JavaDiffUtils.kt | 4 +- .../dependency/lib/JetBrainsAnnotations.kt | 2 +- .../kotlin/io/spine/dependency/lib/Kotlin.kt | 10 +- .../io/spine/dependency/lib/KotlinPoet.kt | 4 +- .../io/spine/dependency/lib/KotlinSemver.kt | 4 +- .../kotlin/io/spine/dependency/lib/Log4j2.kt | 4 +- .../kotlin/io/spine/dependency/lib/Netty.kt | 4 +- .../kotlin/io/spine/dependency/lib/Okio.kt | 4 +- .../dependency/lib/PalantirJavaFormat.kt | 4 +- .../io/spine/dependency/lib/PerfMark.kt | 34 ++ .../kotlin/io/spine/dependency/lib/Plexus.kt | 4 +- .../io/spine/dependency/lib/Protobuf.kt | 6 +- .../kotlin/io/spine/dependency/lib/Roaster.kt | 4 +- .../kotlin/io/spine/dependency/lib/Slf4J.kt | 5 +- .../kotlin/io/spine/dependency/local/Base.kt | 4 +- .../io/spine/dependency/local/Compiler.kt | 8 +- .../io/spine/dependency/local/CoreJvm.kt | 2 +- .../spine/dependency/local/CoreJvmCompiler.kt | 4 +- .../io/spine/dependency/local/Logging.kt | 2 +- .../io/spine/dependency/local/McJava.kt | 79 ---- .../io/spine/dependency/local/ProtoData.kt | 178 -------- .../io/spine/dependency/local/ProtoTap.kt | 6 +- .../kotlin/io/spine/dependency/local/Time.kt | 2 +- .../io/spine/dependency/local/ToolBase.kt | 6 +- .../io/spine/dependency/local/Validation.kt | 10 +- .../kotlin/io/spine/dependency/storage/H2.kt | 40 ++ .../io/spine/dependency/storage/Hikari.kt | 40 ++ .../io/spine/dependency/storage/HsqlDb.kt | 41 ++ .../io/spine/dependency/storage/MySql.kt | 41 ++ .../io/spine/dependency/storage/PostgreSql.kt | 40 ++ .../io/spine/dependency/storage/QueryDsl.kt | 45 ++ .../kotlin/io/spine/dependency/test/JUnit.kt | 6 +- .../kotlin/io/spine/dependency/test/Jacoco.kt | 4 +- .../dependency/test/KotlinCompileTesting.kt | 4 +- .../kotlin/io/spine/dependency/test/Kover.kt | 2 +- .../spine/dependency/test/Testcontainers.kt | 63 +++ .../kotlin/io/spine/dependency/test/Truth.kt | 4 +- .../src/main/kotlin/io/spine/gradle/Cli.kt | 25 +- .../kotlin/io/spine/gradle/ConfigTester.kt | 20 +- .../main/kotlin/io/spine/gradle/RunGradle.kt | 6 +- .../kotlin/io/spine/gradle/SpineTaskGroup.kt | 49 +++ .../io/spine/gradle/VersionComparator.kt | 115 ++++++ .../io/spine/gradle/VersionGradleFile.kt | 157 +++++++ .../io/spine/gradle/dart/DartEnvironment.kt | 6 +- .../io/spine/gradle/dart/DartExtension.kt | 6 +- .../kotlin/io/spine/gradle/dart/task/Build.kt | 9 +- .../io/spine/gradle/dart/task/DartTasks.kt | 19 +- .../spine/gradle/dart/task/IntegrationTest.kt | 8 +- .../io/spine/gradle/dart/task/Publish.kt | 9 +- .../kotlin/io/spine/gradle/git/Repository.kt | 116 +++++- .../github/pages/RepositoryExtensions.kt | 15 +- .../io/spine/gradle/github/pages/SshKey.kt | 4 +- .../io/spine/gradle/github/pages/TaskName.kt | 4 +- .../gradle/github/pages/UpdateGitHubPages.kt | 19 +- .../kotlin/io/spine/gradle/java/Linters.kt | 4 +- .../main/kotlin/io/spine/gradle/java/Tasks.kt | 4 +- .../gradle/javadoc/ExcludeInternalDoclet.kt | 12 +- .../spine/gradle/javascript/JsEnvironment.kt | 4 +- .../io/spine/gradle/javascript/JsExtension.kt | 6 +- .../spine/gradle/javascript/task/Assemble.kt | 13 +- .../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/kotlin/KotlinConfig.kt | 20 +- .../gradle/publish/CheckVersionIncrement.kt | 163 ++++++-- .../publish/CustomPublicationHandler.kt | 4 +- .../io/spine/gradle/publish/GitHubPackages.kt | 8 +- .../io/spine/gradle/publish/IncrementGuard.kt | 195 ++++++--- .../kotlin/io/spine/gradle/publish/JarDsl.kt | 4 +- .../io/spine/gradle/publish/MavenMetadata.kt | 84 ++++ .../gradle/publish/PublicationHandler.kt | 2 +- .../io/spine/gradle/publish/PublishingExts.kt | 41 +- .../spine/gradle/publish/SpinePublishing.kt | 2 +- .../io/spine/gradle/repo/Repositories.kt | 75 +++- .../kotlin/io/spine/gradle/repo/Repository.kt | 8 +- .../gradle/report/coverage/KoverConfig.kt | 390 ++++++++++++++++++ .../gradle/report/coverage/SiblingCoverage.kt | 114 +++++ .../gradle/report/license/LicenseReporter.kt | 22 +- .../report/license/ModuleDataExtensions.kt | 4 +- .../io/spine/gradle/report/license/Paths.kt | 2 +- .../gradle/report/pom/DependencyWriter.kt | 43 +- .../spine/gradle/report/pom/PomGenerator.kt | 3 + .../gradle/report/pom/ScopedDependency.kt | 36 +- .../kotlin/io/spine/gradle/testing/Tasks.kt | 7 +- .../spine/gradle/testing/TestKitCoverage.kt | 159 +++++++ .../src/main/kotlin/jacoco-kmm-jvm.gradle.kts | 17 +- .../main/kotlin/jacoco-kotlin-jvm.gradle.kts | 15 +- .../src/main/kotlin/jvm-module.gradle.kts | 9 +- .../src/main/kotlin/kmp-module.gradle.kts | 6 +- .../main/kotlin/uber-jar-module.gradle.kts | 7 +- .../src/main/kotlin/write-manifest.gradle.kts | 6 +- .../io/spine/gradle/VersionComparatorSpec.kt | 87 ++++ .../io/spine/gradle/VersionGradleFileSpec.kt | 86 ++++ .../gradle/publish/IncrementGuardTest.kt | 222 ++++++++++ .../spine/gradle/publish/MavenMetadataSpec.kt | 53 +++ .../report/coverage/FileExtensionsTest.kt | 128 ++++++ .../gradle/report/pom/DependencyWriterSpec.kt | 312 ++++++++++++++ config | 2 +- docs/project.md | 18 + gradle.properties | 26 +- gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 4 +- gradlew.bat | 4 +- init-submodules | 99 +++++ settings.gradle.kts | 2 +- 229 files changed, 4308 insertions(+), 3059 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/project-structure-expectations.md create mode 120000 .agents/project.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 create mode 160000 .agents/shared create mode 120000 .agents/skills 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/dependency-audit/SKILL.md delete mode 100644 .agents/skills/dependency-update/SKILL.md delete mode 100644 .agents/skills/dependency-update/agents/openai.yaml 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/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/review-docs/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/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/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 .github/copilot-instructions.md create mode 100644 .github/workflows/check-links.yml create mode 100644 .github/workflows/revalidate-versions.yml create mode 100644 .github/workflows/secret-scan.yml create mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/lib/PerfMark.kt delete mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/local/McJava.kt delete mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/local/ProtoData.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/storage/H2.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/storage/Hikari.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/storage/HsqlDb.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/storage/MySql.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/storage/PostgreSql.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/storage/QueryDsl.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/test/Testcontainers.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/SpineTaskGroup.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/VersionComparator.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/VersionGradleFile.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/publish/MavenMetadata.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/KoverConfig.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/SiblingCoverage.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitCoverage.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/VersionComparatorSpec.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/VersionGradleFileSpec.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/publish/IncrementGuardTest.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/publish/MavenMetadataSpec.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/report/coverage/FileExtensionsTest.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/report/pom/DependencyWriterSpec.kt create mode 100644 docs/project.md create mode 100755 init-submodules diff --git a/.agents/_TOC.md b/.agents/_TOC.md deleted file mode 100644 index dc8cde3ce..000000000 --- a/.agents/_TOC.md +++ /dev/null @@ -1,23 +0,0 @@ -# Table of Contents - -1. [Quick Reference Card](quick-reference-card.md) -2. [Project overview](project-overview.md) -3. [Coding guidelines](coding-guidelines.md) -4. [Documentation & comments](documentation-guidelines.md) -5. [Documentation tasks](documentation-tasks.md) -6. [Running builds](running-builds.md) -7. [Version policy](version-policy.md) -8. [Project structure expectations](project-structure-expectations.md) -9. [Testing](testing.md) -10. [Safety rules](safety-rules.md) -11. [Advanced safety rules](advanced-safety-rules.md) -12. [Refactoring guidelines](refactoring-guidelines.md) -13. [Common tasks](common-tasks.md) -14. [Team memory](memory/MEMORY.md) -15. [Task plans](tasks/README.md) -16. [Java to Kotlin conversion](skills/java-to-kotlin/SKILL.md) -17. [Dependency update](skills/dependency-update/SKILL.md) -18. [Documentation review](skills/review-docs/SKILL.md) -19. [Pre-PR checklist](skills/pre-pr/SKILL.md) -20. [Kotlin code review](skills/kotlin-review/SKILL.md) -21. [Dependency audit](skills/dependency-audit/SKILL.md) diff --git a/.agents/advanced-safety-rules.md b/.agents/advanced-safety-rules.md deleted file mode 100644 index e4105813f..000000000 --- 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 12ede97cd..000000000 --- a/.agents/coding-guidelines.md +++ /dev/null @@ -1,39 +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 - -### โŒ Avoid -- Mutable data structures -- Java-style verbosity (builders with setters) -- 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 5ee954d83..000000000 --- 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 6c9c1bae7..000000000 --- a/.agents/documentation-guidelines.md +++ /dev/null @@ -1,14 +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. - -## 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 8ac4660db..000000000 --- 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 000000000..6f9d96637 --- /dev/null +++ b/.agents/guidelines @@ -0,0 +1 @@ +shared/guidelines \ No newline at end of file diff --git a/.agents/project-structure-expectations.md b/.agents/project-structure-expectations.md deleted file mode 100644 index 22a3ab7d6..000000000 --- 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 new file mode 120000 index 000000000..7e0bf9bd3 --- /dev/null +++ b/.agents/project.md @@ -0,0 +1 @@ +../docs/project.md \ No newline at end of file diff --git a/.agents/quick-reference-card.md b/.agents/quick-reference-card.md deleted file mode 100644 index e2be69cb8..000000000 --- a/.agents/quick-reference-card.md +++ /dev/null @@ -1,9 +0,0 @@ -# ๐Ÿ“ Quick Reference Card - -``` -๐Ÿ”‘ Key Information: -- Kotlin/Java project with CQRS architecture -- Follow coding guidelines in Spine Event Engine docs -- Always include tests with code changes -- Version bump required for all PRs -``` diff --git a/.agents/refactoring-guidelines.md b/.agents/refactoring-guidelines.md deleted file mode 100644 index 191db49f5..000000000 --- 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 db0338d6f..000000000 --- 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 08e9b33d1..000000000 --- a/.agents/safety-rules.md +++ /dev/null @@ -1,7 +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. diff --git a/.agents/scripts b/.agents/scripts new file mode 120000 index 000000000..96bf06e12 --- /dev/null +++ b/.agents/scripts @@ -0,0 +1 @@ +shared/scripts \ No newline at end of file diff --git a/.agents/shared b/.agents/shared new file mode 160000 index 000000000..bda77f6a1 --- /dev/null +++ b/.agents/shared @@ -0,0 +1 @@ +Subproject commit bda77f6a178018569f3977878624adde279b41ec diff --git a/.agents/skills b/.agents/skills new file mode 120000 index 000000000..f14734dde --- /dev/null +++ b/.agents/skills @@ -0,0 +1 @@ +shared/skills \ No newline at end of file diff --git a/.agents/skills/bump-gradle/SKILL.md b/.agents/skills/bump-gradle/SKILL.md deleted file mode 100644 index e5d09269f..000000000 --- a/.agents/skills/bump-gradle/SKILL.md +++ /dev/null @@ -1,117 +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. - -## 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. diff --git a/.agents/skills/bump-gradle/agents/openai.yaml b/.agents/skills/bump-gradle/agents/openai.yaml deleted file mode 100644 index 6edf97877..000000000 --- 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 7143c3e9f..000000000 --- a/.agents/skills/bump-version/SKILL.md +++ /dev/null @@ -1,118 +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. - -## 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` - ``` - - 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 12f6e4f9b..000000000 --- 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/dependency-audit/SKILL.md b/.agents/skills/dependency-audit/SKILL.md deleted file mode 100644 index dc52b5246..000000000 --- a/.agents/skills/dependency-audit/SKILL.md +++ /dev/null @@ -1,115 +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. **Scope the diff.** - - Run `git diff --stat ...HEAD -- 'buildSrc/src/main/kotlin/io/spine/dependency/**'` - (or `--staged` if the user is mid-commit) and read the file list. - - If the diff is empty, ask the user which files to audit. - -2. **Read each changed file fully.** Don't trust the hunk in isolation โ€” - `version` constants are often referenced elsewhere in the same file (e.g. - `runtimeVersion` reused as `embeddedVersion`). - -3. **Run the checks below in order. Stop the audit and surface a finding the - moment any check fails.** - -## 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 via `grep -r ':'`. -- **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 a `const val` outright, grep the repo with - `git grep ''` to confirm no caller is left behind. If callers exist, - this is a Must-fix. - -### D. Convention drift -- **Copyright header year.** Every changed file should have a current-year - copyright line. If a file was edited but its copyright says `2024`, flag it - (the user can run `/update-copyright` to fix). -- **GitHub URL comment.** New `lib/` and `kotlinx/` files conventionally - start with `// https://github.com//` above the object. - Recommend it if missing. -- **`@Suppress("unused", "ConstPropertyName")` on the object.** This is the - established style for constant-heavy declarations. - -### E. Cross-cutting checks -- **`local/` deps don't leak.** Spine SDK artifacts in `local/` should not be - declared in `lib/` or `test/` (and vice versa). -- **No mixing Groovy and Kotlin DSL.** All Gradle code in `buildSrc/` must be - `.kt` or `.gradle.kts`. Catch any `.gradle` file slipping in. - -## Output format - -Three sections, in this order: - -- **Must fix** โ€” version downgrades, missing deprecation markers on removed - symbols, broken callers, BOM/version mismatches. -- **Should fix** โ€” convention drift, missing deprecation `ReplaceWith`, - missing copyright update, missing URL comment, naming oddities. -- **Nits** โ€” formatting, ordering, doc-comment polish. - -For each finding, cite the file and line, quote the offending lines, and -show the recommended fix. - -End with a one-line verdict: `APPROVE`, `APPROVE WITH CHANGES`, or -`REQUEST CHANGES`. diff --git a/.agents/skills/dependency-update/SKILL.md b/.agents/skills/dependency-update/SKILL.md deleted file mode 100644 index b863a41f0..000000000 --- a/.agents/skills/dependency-update/SKILL.md +++ /dev/null @@ -1,278 +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 `./gradlew build` (or `./gradlew clean build` if `.proto` files - participate). -3. If any `local/` artifacts moved, run `./gradlew buildDependants` (the - `ConfigTester` task) to confirm downstream repos still build. -4. Commit. The conventional message is - `chore(deps): refresh external versions` (or a more specific subject if - the diff is small). - -## 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 a61198d32..000000000 --- a/.agents/skills/dependency-update/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Dependency Update" - short_description: "Refresh dependency versions, allowing snapshots only for local Spine SDK artifacts" - default_prompt: "Use $dependency-update to walk every dependency object under buildSrc/src/main/kotlin/io/spine/dependency/, find the latest accepted version for each, and update the version constants in place. External scopes use released non-snapshot versions only; dependency/local/ may use snapshots and pre-releases from sibling Spine repos. Use the URL referenced in each file as the source of truth; fall back to Maven metadata and back-fill missing hints when useful." diff --git a/.agents/skills/java-to-kotlin/SKILL.md b/.agents/skills/java-to-kotlin/SKILL.md deleted file mode 100644 index d3abdc2f7..000000000 --- a/.agents/skills/java-to-kotlin/SKILL.md +++ /dev/null @@ -1,51 +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 (`). 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 252920fed..000000000 --- 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 c55c8c49c..000000000 --- a/.agents/skills/kotlin-review/SKILL.md +++ /dev/null @@ -1,62 +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. -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). - - Immutability by default. - - No `!!` without justification. - - No type names in variable names. - - No string duplication โ€” use companion-object constants. - - No mixing Groovy/Kotlin DSL in build logic. - - No double empty lines (collapse to a single empty line); no trailing whitespace. -4. Check safety rules: reflection, telemetry, blocking-in-coroutines, dependency - bumps that weren't requested. -5. Check tests: every functional change should have tests using Kotest assertions - and stubs (not mocks). -6. Check the version gate: - - If the repository has a root `version.gradle.kts`, confirm it was - incremented when the change is user-visible. - - If root `version.gradle.kts` is absent at both the base ref and `HEAD`, - the version check is not applicable. Do not report a missing version bump - or ask for the file to be created. - -## Output format - -Return three sections, in this order: - -- **Must fix** โ€” violations of safety rules, broken builds, missing version - bump when the version gate applies, missing tests for functional changes. -- **Should fix** โ€” coding-guideline violations and clearer idiomatic alternatives. - Cite the specific guideline. -- **Nits** โ€” style and naming suggestions. - -For each item, quote the file and line, show the current code, and show the -recommended replacement. If there's nothing in a section, write "None." - -End with a one-line verdict: `APPROVE`, `APPROVE WITH CHANGES`, or `REQUEST CHANGES`. diff --git a/.agents/skills/move-files/SKILL.md b/.agents/skills/move-files/SKILL.md deleted file mode 100644 index 2885f4828..000000000 --- a/.agents/skills/move-files/SKILL.md +++ /dev/null @@ -1,49 +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. - - Prefer `git mv` for tracked files in the repo. - - 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. - -## 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 ba90a9f8f..000000000 --- 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 ca09c41ff..000000000 --- a/.agents/skills/pre-pr/SKILL.md +++ /dev/null @@ -1,173 +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 the configured - build/check command per `.agents/running-builds.md`, and invoke the - configured reviewers (`kotlin-review`, `review-docs`, `dependency-audit`) - 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` (for example, shared -configuration repositories). 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 based on what - changed. It may be Gradle or another repository-specific command. -- `.agents/safety-rules.md` and `.agents/advanced-safety-rules.md` โ€” hard - constraints checked by the reviewers. -- The reviewer skills/agents themselves: `kotlin-review` (Claude agent), - `review-docs` (skill + Claude agent), `dependency-audit` (Claude agent). - -## Procedure - -Execute the steps in order. If a step fails, stop, write a `FAIL` sentinel -(see step 6), and report the failure โ€” do not run the remaining steps. - -### 1. Determine scope and repository capabilities - -- Base ref: `master` unless the user provides a different one. -- Diff command: `git diff ...HEAD --name-only` for the file list, - `git diff ...HEAD --stat` for the summary. -- Repository root: `git rev-parse --show-toplevel`. -- Version gate: - - Check only the repository-root `version.gradle.kts`. - - If `version.gradle.kts` is absent at both `` and `HEAD`, record the - version check as `N/A` and continue. Do not ask the user to run - `/bump-version`. - - If `version.gradle.kts` exists at `HEAD`, enforce the version check in - step 2. - - If `version.gradle.kts` exists at `` but is missing at `HEAD`, fail - unless the user explicitly asked to migrate the repository away from - Gradle Build Tools versioning. -- Classify the changes: - - **proto** โ€” any `*.proto` file changed. - - **code** โ€” any `*.kt`, `*.kts`, or `*.java` file changed. - - **docs** โ€” any `*.md` file or doc-only edits inside sources changed. - - **deps** โ€” any file under `buildSrc/src/main/kotlin/io/spine/dependency/` - changed. - -### 2. Version-bump check - -- If the version gate is `N/A`, skip this step with note: - "`version.gradle.kts` is absent; this repository is not a versioned Gradle - Build Tools project." -- Otherwise, read `version.gradle.kts` at `HEAD` and, when present, at - ``. -- Confirm the version string is strictly greater (semver + Spine snapshot - rules โ€” see `.agents/version-policy.md`) when both sides have the file. -- If the file is newly introduced at `HEAD`, report the introduced version and - continue. -- If unchanged or decreased, stop with a Must-fix: "Run `/bump-version`." - -### 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` (tests not required) - -If the repository does not have `./gradlew`, do not fail solely because Gradle -is unavailable. Read `.agents/running-builds.md` for the repository-specific -non-Gradle command that matches the change type, and run that instead. If no -build/check command is documented for the change type, record `build=skipped` -with the reason and continue. - -Run the chosen command. Surface the first failing module/task/check. On -failure, stop and write a `FAIL` sentinel. - -### 4. Reviewers (run in parallel) - -Dispatch the relevant reviewers concurrently and collect their verdicts: - -- Always: `kotlin-review` (if **code** changed) and `review-docs` (if - **docs** or KDoc changed). -- If **deps** changed: `dependency-audit`. - -Pass each reviewer the base ref, changed-file list, build/check result, and -version-check result. When the version check is `N/A`, say explicitly: -"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 and emits a Must-fix / Should-fix / Nits -report plus a one-line verdict (`APPROVE`, `APPROVE WITH CHANGES`, or -`REQUEST CHANGES`). - -### 5. Aggregate - -- Overall **PASS** when: - - Version check passed or was `N/A`, - - Build succeeded, - - Every dispatched reviewer returned `APPROVE` or `APPROVE WITH CHANGES` - *and* no Must-fix items remain unaddressed in this session. -- Otherwise **FAIL**. - -### 6. Sentinel - -Write `.git/pre-pr.ok` at the repo root (NOT under `.claude/` โ€” the -sentinel must travel with the local clone, not be checked in). Format: - -``` -head= -branch= -status=PASS|FAIL -timestamp= -build= -reviewers= -version=new, introduced:, or "not-applicable"> -``` - -The `gh pr create` hook (`.claude/scripts/pre-pr-gate.sh`) checks this -file's `head=` and `status=` fields. Extra fields are allowed. The sentinel is -invalidated automatically when HEAD advances โ€” the hook compares the recorded -`head=` against the current HEAD SHA. - -## Output format - -Report in this shape: - -``` -## Pre-PR checklist ( vs ) - -| Check | Status | Notes | -|---------------|--------|----------------------------------------| -| Version check | โ€ฆ | โ†’ , introduced, or N/A | -| Build/check | โ€ฆ | | -| kotlin-review | โ€ฆ | | -| review-docs | โ€ฆ | | -| dep audit | โ€ฆ | | - -**Overall: PASS|FAIL** -Sentinel: .git/pre-pr.ok (status=PASS|FAIL, head=) -``` - -On `PASS`, end with: "You can now run `gh pr create`." -On `FAIL`, end with the specific blocker and the next action. - -## Notes - -- This skill must NOT create the PR itself. It only gates whether the - workspace is ready. -- This skill must NOT create `version.gradle.kts`. Repositories without a root - `version.gradle.kts` are valid; their version check is `N/A`. -- The sentinel lives under `.git/` (untracked by definition) so it is - per-clone and never committed. -- Each reviewer remains the source of truth for its own checks; this - skill does not duplicate their rules โ€” it only orchestrates and - aggregates. diff --git a/.agents/skills/review-docs/SKILL.md b/.agents/skills/review-docs/SKILL.md deleted file mode 100644 index d936fa28a..000000000 --- a/.agents/skills/review-docs/SKILL.md +++ /dev/null @@ -1,129 +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) - - `**/*.md` (Markdown docs) - Do **not** review the full repo โ€” only what changed. - -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. - -### B. Markdown docs - -- **Footnote-style reference links** for external `https://` URLs (per the - `writer` skill). Inline `[label](https://โ€ฆ)` in body prose is a - Should-fix; inline links to local relative paths are fine. -- **Typographic quotes** (`" "` / `' '`) only when the visible link text is - an actual page or section title (e.g., the "Getting started" page). - Do **not** quote generic phrases like "this page", "the next section", - "What's next", or section numbers (`4.3`). -- **Sidenav sync.** If the diff adds/removes/renames/moves a page under - `docs/content/docs/

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

` tags) left in Kotlin KDoc, broken Markdown links. -- **Should fix** โ€” TODO format, inline-comment overuse in production, - inline external links that should be footnote-style, missing typographic - quotes (or unwanted ones), widow/runt/orphan/river paragraphs, - fenced-vs-indented code blocks. -- **Nits** โ€” wording, terminology drift, code-identifier capitalization - in prose, "for the code reviewer" pointers if any code issues surfaced - incidentally. - -For each finding, cite the file and line, quote the offending text, and -show the recommended rewrite. If a section is empty, write "None." - -End with a one-line verdict: `APPROVE`, `APPROVE WITH CHANGES`, or -`REQUEST CHANGES`. diff --git a/.agents/skills/update-copyright/SKILL.md b/.agents/skills/update-copyright/SKILL.md deleted file mode 100644 index 6afc4c7cf..000000000 --- a/.agents/skills/update-copyright/SKILL.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: update-copyright -description: > - Update source file copyright headers from the IntelliJ IDEA copyright profile, - replacing `today.year` with the current year. - Automatically apply when source files are modified in a change set. ---- - -# Copyright Update - -**Command:** `python3 .agents/skills/update-copyright/scripts/update_copyright.py` - -1. Scope: explicit files/dirs from the user, or all tracked source files if none given. -2. No explicit paths โ†’ run with `--dry-run` first, then without. -3. Relay stdout (notice source, file count, changed paths) to the user. -4. Never add a copyright header to a file that does not already have one. diff --git a/.agents/skills/update-copyright/agents/openai.yaml b/.agents/skills/update-copyright/agents/openai.yaml deleted file mode 100644 index 246dd647f..000000000 --- a/.agents/skills/update-copyright/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Copyright Update" - short_description: "Refresh source copyright headers" - default_prompt: "Use $update-copyright to refresh source file copyright headers from the IntelliJ IDEA copyright profile in this repository." diff --git a/.agents/skills/update-copyright/scripts/update_copyright.py b/.agents/skills/update-copyright/scripts/update_copyright.py deleted file mode 100755 index 2dbf8bbc4..000000000 --- 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 8770b3275..000000000 --- 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/writer/SKILL.md b/.agents/skills/writer/SKILL.md deleted file mode 100644 index 6b9d86f88..000000000 --- a/.agents/skills/writer/SKILL.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -name: writer -description: > - 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 44eaa4e24..000000000 --- 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 f405b71e1..000000000 --- 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 fdbd9b6a0..000000000 --- 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 5517516f5..000000000 --- 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 f81bdbf3d..000000000 --- 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 3e8abd549..000000000 --- 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 25885f9d7..000000000 --- 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 24499cc51..000000000 --- a/.claude/commands/pre-pr.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -description: Run the applicable pre-PR checklist (version gate, build/check, reviewers) and write a sentinel so `gh pr create` is unblocked. -argument-hint: "[base-ref]" -allowed-tools: Read, Write, Grep, Glob, Agent, Bash ---- - -Follow the `pre-pr` skill exactly: - -- Skill: `.agents/skills/pre-pr/SKILL.md` -- Base ref: $ARGUMENTS (treat empty as `master`). -- Detect whether the repository-root `version.gradle.kts` exists. If it is - absent at both the base ref and `HEAD`, the version check is `N/A`; do not - create the file and do not ask for `/bump-version`. -- Run the build/check command selected by the skill and - `.agents/running-builds.md`. The command may be Gradle or non-Gradle. -- Dispatch the reviewers as Claude subagents in parallel โ€” send a single - message with multiple Agent tool uses: - - `kotlin-review` when `.kt|.kts|.java` files changed. - - `review-docs` when `.md` files or KDoc inside sources changed. - - `dependency-audit` when any file under - `buildSrc/src/main/kotlin/io/spine/dependency/` changed. -- Pass the version-check status to reviewers. If it is `N/A`, tell them: - "This repository has no root `version.gradle.kts`; a version bump is not - applicable and must not be reported as missing." -- Each reviewer is read-only; do not pass it edit tools. -- On any reviewer returning `REQUEST CHANGES`, treat the overall result - as `FAIL` and stop before writing the sentinel as `PASS`. -- Sentinel location: `$(git rev-parse --show-toplevel)/.git/pre-pr.ok`, - format per the skill (`head=`, `branch=`, `status=`, `timestamp=`, - `build=`, `reviewers=`, `version=`). Use `git rev-parse HEAD` for the - SHA and `date -u +%Y-%m-%dT%H:%M:%SZ` for the timestamp. -- Do NOT run `gh pr create`. That is the user's next step. diff --git a/.claude/commands/review-docs.md b/.claude/commands/review-docs.md deleted file mode 100644 index f8043f0ea..000000000 --- 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 8a8d84ca0..000000000 --- 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 076fb6133..000000000 --- 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 b9b9a742b..000000000 --- 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.json b/.claude/settings.json index 21fd266d0..357650cf7 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -2,6 +2,7 @@ "$schema": "https://json.schemastore.org/claude-code-settings.json", "permissions": { "allow": [ + "Edit(version.gradle.kts)", "Bash(./gradlew:*)", "Bash(./config/gradlew:*)", "Bash(git status:*)", @@ -15,6 +16,7 @@ "Bash(git restore:*)", "Bash(git stash:*)", "Bash(git fetch:*)", + "Bash(git push:*)", "Bash(git rev-parse:*)", "Bash(git ls-files:*)", "Bash(git mv:*)", @@ -34,7 +36,6 @@ "Bash(./config/migrate)" ], "deny": [ - "Bash(git push:*)", "Bash(git reset --hard:*)", "Bash(git clean -fdx:*)", "Bash(rm -rf /:*)", @@ -53,22 +54,31 @@ ] }, "hooks": { - "PreToolUse": [ + "SessionStart": [ { - "matcher": "Edit|Write|MultiEdit", "hooks": [ { "type": "command", - "command": "$CLAUDE_PROJECT_DIR/.claude/scripts/protect-version-file.sh" + "command": "$CLAUDE_PROJECT_DIR/init-submodules" } ] - }, + } + ], + "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", - "command": "$CLAUDE_PROJECT_DIR/.claude/scripts/pre-pr-gate.sh" + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/secret-scan-gate.sh" + }, + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/pre-pr-gate.sh" + }, + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/publish-version-gate.sh" } ] } @@ -79,7 +89,11 @@ "hooks": [ { "type": "command", - "command": "$CLAUDE_PROJECT_DIR/.claude/scripts/sanitize-source-code.kt" + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/sanitize-source-code.sh" + }, + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/update-copyright.sh" } ] } diff --git a/.codecov.yml b/.codecov.yml index b5739f89b..e05fc77b6 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -12,15 +12,15 @@ coverage: - "**/test/**/*" status: # https://docs.codecov.com/docs/github-checks#yaml-configuration-for-github-checks-and-codecov - patch: false + patch: off # https://docs.codecov.com/docs/commit-status project: default: target: auto threshold: 0.05% - base: auto paths: - - "src" + - "src/main/**/*" + - "**/src/main/**" if_ci_failed: error informational: false only_pulls: true diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..039657bee --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,45 @@ +# GitHub Copilot Instructions + +## Repository context + +This repository is part of the Spine SDK organisation (~40 repos). + +Universal agent instructions are in [`AGENTS.md`](../AGENTS.md) at the +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/guidelines/` โ€” see +`.agents/guidelines/_TOC.md` for the index. + +## Do not review + +Never review `gradlew` or `gradlew.bat` in any repository, including `config`. +These files are provided by Gradle and are not edited manually. + +If the current repository is `config`, review its files normally unless noted +above: they are authoritative there. In other repositories, the following files are managed by +the `config` submodule and must be reviewed in the `config` repository, not +here. In those consumer repositories, skip them without comment: + +- `AGENTS.md`, `CLAUDE.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md` +- `.agents/**` (except `.agents/project.md`) +- `.claude/**`, `.idea/**`, `.junie/**` +- `.github/copilot-instructions.md` +- `buildSrc/**` (except `buildSrc/src/main/kotlin/module.gradle.kts`) +- `gradle/`, `gradlew`, `gradlew.bat` +- `.codecov.yml`, `.gitignore`, `gradle.properties`, `lychee.toml` +- `.github/workflows/` โ€” unless the workflow was introduced by this repo + +## Universal rules + +**Do not suggest:** +- Any git history operation โ€” `git commit`, `git push`, `git tag`, + `git rebase`, `git merge`, `git cherry-pick`, `gh pr merge`, or any other + command that writes to history โ€” leave these to the developer. +- Auto-updating dependency versions outside a dedicated update task. +- Feature flags, backwards-compatibility shims, or fallbacks for scenarios + that cannot occur in the current codebase. +- Analytics, telemetry, or tracking code. +- Reflection or unsafe code without explicit approval. diff --git a/.github/workflows/build-on-ubuntu.yml b/.github/workflows/build-on-ubuntu.yml index cd6b93714..b07bf0fb0 100644 --- a/.github/workflows/build-on-ubuntu.yml +++ b/.github/workflows/build-on-ubuntu.yml @@ -1,27 +1,59 @@ name: Ubuntu CI -on: push +# Triggers: +# * push to a default or release-line branch โ€” those ending in `master` or +# `main`, the same set `increment-guard.yml` guards as PR bases (e.g. +# `master`, `2.x-jdk8-master`). These post-merge runs are the only source +# of base-branch coverage, since `Publish` runs `publish -x test` and +# uploads none; `target: auto` in `.codecov.yml` compares each pull request +# against its base baseline. +# * pull_request โ€” gates a change on its merge result, not the branch tip. +on: + push: + branches: + - '**master' + - '**main' + pull_request: jobs: build: name: Build on Ubuntu runs-on: ubuntu-latest + concurrency: # Avoid canceling in-progress runs for the same branch. + group: ubuntu-ci-${{ github.ref }} + cancel-in-progress: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: 'true' - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: java-version: 17 distribution: zulu - cache: gradle - - name: Build project and run tests + - uses: gradle/actions/setup-gradle@v6 + + # Mirrors the pagefile step in build-on-windows.yml. The Linux runner + # ships with effectively no swap, so a memory peak becomes an instant + # OOM kill; this gives the kernel somewhere to fall back to. + - name: Add swap space + uses: pierotofy/set-swap-space@v1.0 + with: + swap-size-gb: 8 + + - name: Build project, run tests shell: bash run: ./gradlew build --stacktrace + # `build` does not run Dokka โ€” its tasks are gated to the publishing + # graph โ€” so `dokkaGenerate` is appended to surface documentation + # warnings before merge, instead of only in the post-merge `Publish` + # job. `failOnWarning` is enabled in the Dokka setup. + - name: Check documentation + run: ./gradlew dokkaGenerate --stacktrace + # See: https://github.com/marketplace/actions/junit-report-action - name: Publish Test Report uses: mikepenz/action-junit-report@v4.0.3 @@ -30,9 +62,33 @@ jobs: report_paths: '**/build/test-results/**/TEST-*.xml' require_tests: true # will fail workflow if test reports not found + # Probe whether the upload token is available without exposing its value + # to the build/test steps. Scoping `CODECOV_TOKEN` to this trivial step + # (and to the upload step's `with.token`) keeps PR-authored code in + # `./gradlew build` from ever seeing the secret. The `secrets` context is + # not available in a step `if:`, so the upload gates on this output. + - name: Detect Codecov token + id: codecov + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + run: | + if [ -n "$CODECOV_TOKEN" ]; then + echo "available=true" >> "$GITHUB_OUTPUT" + else + echo "available=false" >> "$GITHUB_OUTPUT" + fi + + # On `push` (master) always upload โ€” these runs are the only source of + # the `target: auto` baseline, so an absent token there is a + # misconfiguration that should fail loudly rather than silently stop + # refreshing coverage. On `pull_request`, skip when the token is absent: + # forked and Dependabot PRs run without secrets, and `fail_ci_if_error` + # would otherwise redden a healthy PR (coverage gating is meaningless + # there anyway). - name: Upload code coverage report - uses: codecov/codecov-action@v4 + if: steps.codecov.outputs.available == 'true' || github.event_name == 'push' + uses: codecov/codecov-action@v7 with: token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: false + fail_ci_if_error: true verbose: true diff --git a/.github/workflows/build-on-windows.yml b/.github/workflows/build-on-windows.yml index 91a0bfef3..0a07cfab6 100644 --- a/.github/workflows/build-on-windows.yml +++ b/.github/workflows/build-on-windows.yml @@ -8,15 +8,17 @@ jobs: name: Build on Windows steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: - submodules: 'true' + submodules: recursive + fetch-depth: 0 - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: java-version: 17 distribution: zulu - cache: gradle + + - uses: gradle/actions/setup-gradle@v6 # See: https://github.com/al-cheb/configure-pagefile-action - name: Configure Pagefile @@ -24,8 +26,7 @@ jobs: - name: Build project and run tests shell: cmd - # For the reason on `--no-daemon` see https://github.com/actions/cache/issues/454 - run: gradlew.bat build --stacktrace --no-daemon + run: gradlew.bat build --stacktrace # See: https://github.com/marketplace/actions/junit-report-action - name: Publish Test Report @@ -33,4 +34,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: true diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml new file mode 100644 index 000000000..6755ff910 --- /dev/null +++ b/.github/workflows/check-links.yml @@ -0,0 +1,216 @@ +name: Check Links + +# Trigger only when the docs site, the link checker config, or this workflow +# itself changes โ€” unrelated PRs do not need to pay the build+check cost. +on: + pull_request: + paths: + - 'docs/**' + - 'site/**' + - 'lychee.toml' + - '.github/workflows/check-links.yml' + workflow_dispatch: + +env: + HUGO_VERSION: 0.161.1 + LYCHEE_RELEASE: "lychee-x86_64-unknown-linux-gnu.tar.gz" + LYCHEE_VERSION_TAG: "lychee-v0.24.2" + # SHA256 of the above tarball, pinned at download time. Update alongside + # LYCHEE_VERSION_TAG whenever the binary is upgraded. + LYCHEE_SHA256: "1f4e0ef7f6554a6ed33dd7ac144fb2e1bbed98598e7af973042fc5cd43951c9a" + # Force Hugo to write its module cache where the cache step actually + # restores from. Hugo's default on Linux is `~/.cache/hugo_cache` + # (or `$TMPDIR/hugo_cache_$USER`), neither of which matches the + # `path: /tmp/hugo_cache` cache step below โ€” without this env var, + # the cache would silently never hit. + HUGO_CACHEDIR: /tmp/hugo_cache + +jobs: + check-links: + runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + steps: + - name: Checkout + uses: actions/checkout@v6 + + # Detect the Hugo site root (`docs/` or `site/`) by looking for a Hugo + # config file. Hugo config may live directly in the site root or in a + # `config/` or `config/_default/` subdirectory (both layouts are valid). + # Outputs `present=true|false` and `work_dir` (the directory where + # `npm ci` / `hugo` commands should run โ€” either `$dir/_preview` for + # repos that use a separate preview sub-tree, or `$dir` for repos whose + # Node/Hugo setup lives at the site root). + # When neither directory has a Hugo config, the job short-circuits to a + # success so that this shared workflow stays green on repos that do not + # host a Hugo site at all. + - name: Detect docs site + id: docs + run: | + 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 + if [ -f "$dir/_preview/package-lock.json" ]; then + echo "work_dir=$dir/_preview" >> "$GITHUB_OUTPUT" + echo "present=true" >> "$GITHUB_OUTPUT" + echo "::notice::Hugo site found under $dir/ (work_dir: $dir/_preview)" + elif [ -f "$dir/package-lock.json" ]; then + echo "work_dir=$dir" >> "$GITHUB_OUTPUT" + echo "present=true" >> "$GITHUB_OUTPUT" + echo "::notice::Hugo site found under $dir/ (work_dir: $dir)" + else + echo "present=false" >> "$GITHUB_OUTPUT" + echo "::notice::Hugo config found in $dir/ but no package-lock.json found โ€” skipping link check." + fi + exit 0 + fi + done + done + echo "present=false" >> "$GITHUB_OUTPUT" + echo "::notice::No Hugo site found under docs/ or site/ โ€” skipping link check." + + - name: Setup Hugo + if: steps.docs.outputs.present == 'true' + uses: peaceiris/actions-hugo@v3 + with: + hugo-version: ${{ env.HUGO_VERSION }} + extended: true + + # `actions/setup-node@v4` ships with built-in npm caching that hashes + # the lockfile and restores `~/.npm`. We use that instead of a + # standalone `actions/cache@v4` block so there is only one source of + # truth for the cache key (no drift between two layers). + - name: Setup Node + if: steps.docs.outputs.present == 'true' + uses: actions/setup-node@v4 + with: + node-version: '26' + cache: 'npm' + cache-dependency-path: ${{ steps.docs.outputs.work_dir }}/package-lock.json + + # `HUGO_CACHEDIR=/tmp/hugo_cache` (set in `env:` above) makes Hugo + # actually write to the path this step restores from. The key hashes + # both possible go.sum locations so adding/removing a Hugo module + # invalidates the cache deterministically regardless of site root. + - name: Cache Hugo Modules + if: steps.docs.outputs.present == 'true' + uses: actions/cache@v4 + with: + path: /tmp/hugo_cache + key: ${{ runner.os }}-hugomod-${{ hashFiles('docs/**/go.sum', 'site/**/go.sum') }} + restore-keys: | + ${{ runner.os }}-hugomod- + + - name: Install Dependencies + if: steps.docs.outputs.present == 'true' + working-directory: ${{ steps.docs.outputs.work_dir }} + run: npm ci + + - name: Build docs preview site + if: steps.docs.outputs.present == 'true' + working-directory: ${{ steps.docs.outputs.work_dir }} + run: hugo -e development + + # Cache Lychee results to avoid hitting rate limits. + # Key on the lychee.toml hash so that exclude-list edits (e.g. removing + # an exclude pattern) invalidate the cache deterministically; otherwise + # stale `200 OK` entries for the now-checked URLs would be trusted until + # `max_cache_age` expires. + - name: Cache Lychee results + if: steps.docs.outputs.present == 'true' + uses: actions/cache@v4 + with: + path: .lycheecache + key: cache-lychee-${{ runner.os }}-${{ hashFiles('lychee.toml') }} + restore-keys: | + cache-lychee-${{ runner.os }}- + + # The cache key includes LYCHEE_VERSION_TAG so a version bump + # automatically pulls a fresh binary instead of reusing the old one. + # The restore-keys fallback lets a release-filename tweak (rare) reuse + # the existing cached binary for the same version-tag instead of paying + # for a fresh download. + - name: Cache Lychee executable + if: steps.docs.outputs.present == 'true' + id: cache-lychee + uses: actions/cache@v4 + with: + path: lychee + key: ${{ runner.os }}-${{ env.LYCHEE_VERSION_TAG }}-${{ env.LYCHEE_RELEASE }} + restore-keys: | + ${{ runner.os }}-${{ env.LYCHEE_VERSION_TAG }}- + + # We use Lychee directly instead of a GitHub Action because it + # must have access to the local Hugo server, which is not visible + # from the Docker-based action. + # + # `if:` gating uses `hashFiles('lychee/lychee')` rather than + # `steps.cache-lychee.outputs.cache-hit != 'true'`. Per `actions/cache` + # docs, `cache-hit` is only `'true'` on an EXACT key match โ€” a restore + # via `restore-keys` reports `cache-hit == 'false'`, even though the + # binary is present in the workspace. Re-downloading in that case + # would defeat the point of the fallback. `hashFiles` returns an empty + # string when the file is absent, so this guard runs the download iff + # neither the exact key nor any restore-key restored the binary. + - name: Download Lychee executable + uses: robinraju/release-downloader@v1.7 + if: steps.docs.outputs.present == 'true' && hashFiles('lychee/lychee') == '' + with: + repository: "lycheeverse/lychee" + tag: ${{ env.LYCHEE_VERSION_TAG }} + fileName: ${{ env.LYCHEE_RELEASE }} + + - name: Verify Lychee checksum + if: steps.docs.outputs.present == 'true' && hashFiles('lychee/lychee') == '' + run: | + echo "${{ env.LYCHEE_SHA256 }} ${{ env.LYCHEE_RELEASE }}" | sha256sum --check --strict + + # The v0.24.2 tarball contains a top-level directory + # (e.g. `lychee-x86_64-unknown-linux-gnu/lychee`), so `--strip-components=1` + # flattens it to `lychee/lychee` โ€” matching what the companion + # `check-links` skill does locally and what the next step expects. + - name: Extract Lychee executable + if: steps.docs.outputs.present == 'true' && hashFiles('lychee/lychee') == '' + run: | + mkdir -p lychee && + tar -xzf ${{ env.LYCHEE_RELEASE }} --strip-components=1 -C lychee + + # 1. In the generated HTML, some inner links will have absolute URLs and + # the link checker will attempt to fetch them. That's why we need + # a server. Sadly, link checkers have no settings to address this. + # 2. Output redirection is necessary for nohup in GitHub Actions. + # 3. Sleep + `curl` readiness check make sure the server is actually + # serving HTTP before the next step runs Lychee. Without the curl + # probe a silent startup failure (port already bound, missing + # Hugo module, build error surfacing after `nohup` returns 0) + # would manifest 60 s later as "every URL unreachable" Lychee + # errors instead of pointing at the real cause. Mirrors the + # `pgrep -F` guard in the companion `check-links` skill. + # 4. `--port 1313` is set explicitly (not relying on Hugo's default) so + # the coupling with `--base-url http://localhost:1313/` in the next + # Lychee step is visible โ€” change one, change the other. + - name: Start Hugo server + if: steps.docs.outputs.present == 'true' + working-directory: ${{ steps.docs.outputs.work_dir }} + run: | + nohup hugo server \ + --environment development \ + --port 1313 \ + > nohup.out 2> nohup.err < /dev/null & + sleep 5 + if ! curl -sf http://localhost:1313/ > /dev/null; then + echo "ERROR: Hugo server did not respond on port 1313." >&2 + echo "--- stdout ---" >&2; cat nohup.out >&2 || true + echo "--- stderr ---" >&2; cat nohup.err >&2 || true + exit 1 + fi + + - name: Check links + if: steps.docs.outputs.present == 'true' + run: | + ./lychee/lychee --config lychee.toml --timeout 60 \ + --base-url http://localhost:1313/ \ + '${{ steps.docs.outputs.work_dir }}/public/**/*.html' diff --git a/.github/workflows/ensure-reports-updated.yml b/.github/workflows/ensure-reports-updated.yml index 315cd202b..20deb3f69 100644 --- a/.github/workflows/ensure-reports-updated.yml +++ b/.github/workflows/ensure-reports-updated.yml @@ -1,19 +1,29 @@ # Ensures that the license report files were modified in this PR. +# +# The check runs only for pull requests targeting a default (`master`/`main`) or +# a release-line (e.g. `2.x-jdk8-master`) branch. The report files embed the project +# version, so they are refreshed by the branches which bump it. Pull requests +# targeting auxiliary branches are not checked. +# +# The base branch is checked inside the job rather than via the `branches` filter: +# a workflow skipped by branch filtering leaves its check in the `Pending` state, +# blocking PRs which require it, while a job skipped via `if` reports `skipped`, +# which satisfies required status checks. name: License Reports on: pull_request: - branches: - - '**' jobs: check: name: Ensure license reports are updated runs-on: ubuntu-latest + # Default and release-line branches, e.g. `master`, `main`, `2.x-jdk8-master`. + if: endsWith(github.base_ref, 'master') || endsWith(github.base_ref, 'main') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: # Configure the checkout of all branches so that it is possible to run the comparison. fetch-depth: 0 diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 50eb05eb1..fc0872b4c 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout latest code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Validate Gradle Wrapper uses: gradle/actions/wrapper-validation@v4 diff --git a/.github/workflows/increment-guard.yml b/.github/workflows/increment-guard.yml index 38ce6f4d3..9a6333f0f 100644 --- a/.github/workflows/increment-guard.yml +++ b/.github/workflows/increment-guard.yml @@ -1,29 +1,100 @@ -# Ensures that the current lib version is not yet published but executing the Gradle -# `checkVersionIncrement` task. +# Guards the project version by executing the Gradle `checkVersionIncrement` task, +# which verifies that the version is both (a) strictly greater than the base branch +# version in `version.gradle.kts` and (b) not already published. The result is +# published as the `Version Guard` commit status โ€” the context required by branch +# protection and re-published by `revalidate-versions.yml` on the heads of other open +# PRs when the base branch advances (so a stale duplicate bump turns red before merge). +# +# The check runs only for pull requests targeting a default (`master`/`main`) or +# a release-line (e.g. `2.x-jdk8-master`) branch. It is the responsibility of a branch +# which aims to merge into such a branch to bump the version. Auxiliary branches +# do not deal with the versions in the release cycle and are not guarded. +# +# The base branch is checked inside the job rather than via the `branches` filter: +# a workflow skipped by branch filtering leaves its check in the `Pending` state, +# blocking PRs which require it, while a job skipped via `if` reports `skipped`, +# which satisfies required status checks. name: Version Guard on: - push: - branches: - - '**' + pull_request: + # Beyond the default activity types (`opened`, `synchronize`, `reopened`), two more are + # needed because they change what the guard must compare against without a new head SHA, + # which would otherwise leave a stale-green `Version Guard` status mergeable: + # * `ready_for_review` โ€” a draft that went stale while in draft becomes ready; and + # * `edited` โ€” the base branch is retargeted (e.g. a release line -> `master`), so the + # strict comparison must be recomputed against the new base. + types: [opened, synchronize, reopened, ready_for_review, edited] jobs: check: name: Check version increment runs-on: ubuntu-latest + # Default and release-line branches, e.g. `master`, `main`, `2.x-jdk8-master`. For an + # `edited` event, run only when the base actually changed (a retarget carries + # `changes.base.ref.from`); title/body edits carry no `changes.base` and are skipped, so + # the guard is not rebuilt needlessly. + if: >- + (endsWith(github.base_ref, 'master') || endsWith(github.base_ref, 'main')) + && (github.event.action != 'edited' || github.event.changes.base.ref.from != '') + + # `statuses: write` lets the job publish the `Version Guard` commit status that + # branch protection requires. + permissions: + contents: read + statuses: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: 'true' - - uses: actions/setup-java@v4 + # `checkVersionIncrement` reads `origin/:version.gradle.kts`. The pull request + # checkout does not include the base branch, so fetch its tip into the expected ref. + - name: Fetch the base branch + shell: bash + run: | + git fetch --no-tags --depth=1 \ + origin "+refs/heads/${GITHUB_BASE_REF}:refs/remotes/origin/${GITHUB_BASE_REF}" + + - uses: actions/setup-java@v5 with: java-version: 17 distribution: zulu - cache: gradle - - name: Check version is not yet published + - uses: gradle/actions/setup-gradle@v6 + + - name: Check version increment + id: guard shell: bash + # `VERSION_GUARD` enables the strict base-branch comparison in `checkVersionIncrement`. + # Only this workflow fetches the base ref (the step above), so the comparison is gated + # to it: other CI builds pull the task in via `publishToMavenLocal` on a shallow + # checkout and must not attempt to read `origin/`. + env: + VERSION_GUARD: "true" run: ./gradlew checkVersionIncrement --stacktrace + + # Publish the verdict as the `Version Guard` commit status on the PR head. Posting it + # on every run (success or failure) is what lets a later re-bump clear a failure that + # `revalidate-versions.yml` set when the base branch advanced. + # + # Skipped for fork PRs: `GITHUB_TOKEN` is read-only for them, so the status cannot be + # posted (and a fork head SHA is not in this repo). The Spine agent workflow pushes PR + # branches to the same repository; fork contributions are handled by a maintainer, who + # owns the version bump. + - name: Report the Version Guard status + if: always() && github.event.pull_request.head.repo.fork == false + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + state=failure + if [ "${{ steps.guard.outcome }}" = "success" ]; then + state=success + fi + gh api -X POST "repos/${{ github.repository }}/statuses/${{ github.event.pull_request.head.sha }}" \ + -f state="${state}" \ + -f context="Version Guard" \ + -f description="Version increment check" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f7218c618..27c11c0f8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,15 +10,16 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: 'true' - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: java-version: 17 distribution: zulu - cache: gradle + + - uses: gradle/actions/setup-gradle@v6 - name: Decrypt CloudRepo credentials run: ./config/scripts/decrypt.sh "$CLOUDREPO_CREDENTIALS_KEY" ./.github/keys/cloudrepo.properties.gpg ./cloudrepo.properties @@ -63,3 +64,18 @@ jobs: REPO_SLUG: ${{ github.repository }} # e.g. SpineEventEngine/core-jvm GOOGLE_APPLICATION_CREDENTIALS: ./maven-publisher.json NPM_TOKEN: ${{ secrets.NPM_SECRET }} + + # A failed publication on `master` is most often a version collision: a stale + # duplicate bump merged before `revalidate-versions.yml` could turn it red (the + # narrow auto-merge race). The artifact is safe โ€” the registry rejects the + # overwrite โ€” but the fix needs a human/agent, so make the failure loud and + # actionable instead of a quiet red run. + - name: Report a failed publication + if: failure() + shell: bash + run: | + echo "::error title=Publish failed::Publishing to Maven failed on the base branch. If this is a version collision, the version is already published (immutable). Bump 'version.gradle.kts' on the base branch (e.g. via a small PR) and re-run this workflow." + echo "Publish failed. If the cause is a version collision:" + echo " 1. Bump 'version.gradle.kts' on the base branch to the next free version." + echo " 2. Re-run this 'Publish' workflow." + echo "Stale duplicate bumps are normally caught before merge by 'revalidate-versions.yml'." diff --git a/.github/workflows/remove-obsolete-artifacts-from-packages.yaml b/.github/workflows/remove-obsolete-artifacts-from-packages.yaml index f70617100..62242c7bb 100644 --- a/.github/workflows/remove-obsolete-artifacts-from-packages.yaml +++ b/.github/workflows/remove-obsolete-artifacts-from-packages.yaml @@ -39,7 +39,7 @@ jobs: outputs: package-names: ${{ steps.request-package-names.outputs.package-names }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: 'true' diff --git a/.github/workflows/revalidate-versions.yml b/.github/workflows/revalidate-versions.yml new file mode 100644 index 000000000..2d2ecb9c6 --- /dev/null +++ b/.github/workflows/revalidate-versions.yml @@ -0,0 +1,57 @@ +# Re-judges every other open pull request when the base branch advances. +# +# Publishing runs on every push to a release base branch, so once one pull request merges +# and bumps the version, any other open PR that bumped to the same (or a lower) value is +# now stale: its publish would collide. GitHub does not re-run a PR's checks when its base +# advances, so this workflow does it actively โ€” for each other open PR whose +# `version.gradle.kts` version is `<=` the new base version, it posts a failing +# `Version Guard` commit status on the PR head, blocking the merge until the author +# re-bumps. The status self-clears: the re-bump push runs `increment-guard.yml`, which +# posts a fresh `success` on the new head. +# +# This narrows, but does not close, the race against auto-merge. A PR that is already +# mergeable can merge in the seconds before this fan-out marks it stale; that late merge +# produces a publish collision which the immutable Maven registry rejects (a loud, +# recoverable red Publish), never an overwrite. The deterministic guarantee is the +# registry's immutability, not this signal. + +name: Revalidate Versions + +on: + push: + # Matches the PR guard's `endsWith(base_ref, 'master'|'main')` and the same scope as + # `build-on-ubuntu.yml`. `**` (unlike `*`) also crosses `/`, so slash-named release + # lines such as `release/2.x-master` are covered. Keeping these definitions identical + # ensures every branch guarded on the PR side is also revalidated here. + branches: + - '**master' + - '**main' + +permissions: + contents: read + statuses: write + pull-requests: read + +concurrency: + # Only the newest tip of a given base matters; a later push to the same ref cancels an + # in-flight fan-out for it. Different bases run independently. + group: revalidate-versions-${{ github.ref }} + cancel-in-progress: true + +jobs: + revalidate: + name: Revalidate open PRs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: 'true' + + - name: Revalidate open PRs against the new base version + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + BASE_REF: ${{ github.ref_name }} + # Invoked via `bash` so it does not depend on the script's committed executable bit. + run: bash ./config/scripts/revalidate-versions.sh diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml new file mode 100644 index 000000000..6f0130e21 --- /dev/null +++ b/.github/workflows/secret-scan.yml @@ -0,0 +1,70 @@ +name: Secret scan + +# Defense-in-depth behind the local `secret-scan` pre-commit hook and the +# `.gitignore` secret patterns: if a credential is committed despite those, this +# fails the pull request before it can merge. Distributed to every Spine repo by +# `./config/pull`. + +on: + pull_request: + push: + branches: + - master + - main + +permissions: + contents: read + +jobs: + gitleaks: + name: gitleaks + runs-on: ubuntu-latest + env: + # Pinned gitleaks version โ€” bump through the usual dependency-update process. + GITLEAKS_VERSION: "8.21.2" + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + # Full history so a pull request's commit range can be scanned. + fetch-depth: 0 + + - name: Install gitleaks + # Run gitleaks as the runner user against the checkout it owns โ€” no + # container, so no "dubious ownership" git error and no GitHub Action + # org-licence requirement. + run: | + curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ + | tar -xzf - gitleaks + ./gitleaks version + + - name: Scan + env: + EVENT: ${{ github.event_name }} + BASE: ${{ github.event.pull_request.base.sha }} + HEAD: ${{ github.event.pull_request.head.sha }} + BEFORE: ${{ github.event.before }} + AFTER: ${{ github.sha }} + run: | + if [ "$EVENT" = pull_request ]; then + # Scan the PR's own commit RANGE: a secret added in one commit and + # deleted in a later commit of the same PR is still caught (a + # working-tree scan would miss it, yet merging keeps the secret-bearing + # commit reachable), while already-rotated secrets in older history + # outside base..head are not re-flagged. + ./gitleaks git --log-opts="$BASE..$HEAD" --redact --verbose --exit-code=1 . + else + # Push to a default branch: scan the pushed commit RANGE (before..after) + # so an add-then-remove batch is caught here too, not only on PRs โ€” the + # leaked commit would otherwise stay reachable on the default branch. A + # branch's first push reports an all-zero `before` (no range); fall back + # to a working-tree scan then. + if [ -n "$BEFORE" ] && [ "$BEFORE" != "0000000000000000000000000000000000000000" ]; then + ./gitleaks git --log-opts="$BEFORE..$AFTER" --redact --verbose --exit-code=1 . + else + # Branch's first push (all-zero `before`): no range to diff against, so + # scan the whole history reachable from the pushed tip as the initial + # import โ€” an add-then-remove within those commits is still caught. + ./gitleaks git --log-opts="$AFTER" --redact --verbose --exit-code=1 . + fi + fi diff --git a/.gitignore b/.gitignore index c43c66220..dfb4774d4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +# >>> shared config (managed by ./config/pull -- do not edit inside this block) >>> # # Copyright 2025, TeamDev. All rights reserved. # @@ -103,14 +104,48 @@ gradle-app.setting # Spine internal directory for storing intermediate artifacts **/.spine/** -# Login details to Maven repository. -# Each workstation should have developer's login defined in this file. +# --------------------------------------------------------------------------- +# Secrets โ€” NEVER commit these. +# +# Encrypted credentials live under `.github/keys/*.gpg` and ARE committed. +# `config/scripts/decrypt.sh` turns each into its PLAINTEXT twin at build / CI / +# publish time (e.g. `spine-dev-framework-ci.json.gpg` -> `spine-dev.json`). The +# decrypted twins below โ€” and any private key or service-account file โ€” must stay +# out of Git. The shared `secret-scan` pre-commit hook is the backstop if one ever +# slips past these patterns. +# --------------------------------------------------------------------------- + +# Maven repository login details; each workstation defines its own. credentials.tar credentials.properties cloudrepo.properties deploy_key_rsa gcs-auth-key.json +# Decrypted Google / GCP service-account keys (plaintext twins of *.gpg). +spine-dev.json +spine-dev-*.json +maven-publisher.json +firebase-sa.json +*-sa.json +*service-account*.json + +# Decrypted credential property files and portal / publisher secrets. +*.secret.properties + +# Private SSH keys (public keys are *.pub and remain committable). +*_rsa +*_dsa +*_ecdsa +*_ed25519 +id_rsa +id_dsa +id_ecdsa +id_ed25519 + +# ...but always keep the committed ENCRYPTED forms. +!*.gpg + # Log files *.log @@ -138,3 +173,52 @@ __pycache__/ # Claude working files /.claude/worktrees/ + +# Auto-downloaded Lychee binary used by the `check-links` skill. +/.agents/skills/check-links/.cache/ + +# Lychee link-checker cache (created by the `check-links` skill and +# the `Check Links` workflow when run locally). +.lycheecache + +# Hugo docs preview site build artifacts (used by the `check-links` +# skill and the `Check Links` workflow in repos that contain a +# `docs/_preview` Hugo site). +docs/_preview/node_modules/ +docs/_preview/public/ +docs/_preview/resources/ +# <<< shared config <<< + +# >>> repo-local entries (preserved across ./config/pull) >>> +!.idea/misc.xml +!.idea/codeStyleSettings.xml +!.idea/codeStyles/ +!.idea/copyright/ +!**/src/**/build/** +!gradle-wrapper.jar +# Login details to Maven repository. +# Each workstation should have developer's login defined in this file. +# <<< repo-local entries <<< + +# >>> secret ignores re-asserted last (managed by ./config/pull -- do not edit) >>> +credentials.tar +credentials.properties +cloudrepo.properties +deploy_key_rsa +gcs-auth-key.json +spine-dev.json +spine-dev-*.json +maven-publisher.json +firebase-sa.json +*-sa.json +*service-account*.json +*.secret.properties +*_rsa +*_dsa +*_ecdsa +*_ed25519 +id_rsa +id_dsa +id_ecdsa +id_ed25519 +# <<< secret ignores <<< diff --git a/.gitmodules b/.gitmodules index 94e8664d6..1978333b9 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 = merge + ignore = all diff --git a/.idea/live-templates/README.md b/.idea/live-templates/README.md index 66713b347..950066731 100644 --- a/.idea/live-templates/README.md +++ b/.idea/live-templates/README.md @@ -5,12 +5,12 @@ This directory contains two live template groups: 1. `Spine.xml`: shortcuts for the repeated patterns used in the framework. 2. `User.xml`: a single shortcut to generate TODO comments. -### Instlallation +### Installation Live templates are not picked up by IDEA automatically. They should be added manually. In order to add these templates, perform the following steps: -1. Copy `*.xml` files from this directory to `templates` directory in the IntelliJ IDEA +1. Copy `*.xml` files from this directory to `templates` directory in the IntelliJ IDEA [settings folder][settings_folder]. 2. Restart IntelliJ IDEA: `File -> Invalidate Caches -> Just restart`. 3. Go to `Preferences -> Editor -> Live Templates`. @@ -23,5 +23,5 @@ In order to add these templates, perform the following steps: 1. Open the corresponding template: `Preferences -> Editor -> Live Templates -> User.todo`. 2. Click on `Edit variables`. 3. Set `USER` variable to your domain email address without `@teamdev.com` ending. For example, - for `jack.sparrow@teamdev.com` use the follwoing expression `"jack.sparrow"`. + for `jack.sparrow@teamdev.com` use the following expression `"jack.sparrow"`. 4. Verify that the template generates expected comments: `// TODO:2022-11-03:jack.sparrow: <...>`. diff --git a/.idea/live-templates/User.xml b/.idea/live-templates/User.xml index cc156507e..958e2ea9b 100644 --- a/.idea/live-templates/User.xml +++ b/.idea/live-templates/User.xml @@ -1,7 +1,7 @@