diff --git a/.agents/tasks/archive/938-move-codegen-request-writer.md b/.agents/tasks/archive/938-move-codegen-request-writer.md new file mode 100644 index 000000000..e32962b5a --- /dev/null +++ b/.agents/tasks/archive/938-move-codegen-request-writer.md @@ -0,0 +1,48 @@ +--- +slug: 938-move-codegen-request-writer +branch: claude/busy-dirac-wwflbm +owner: claude +status: in-progress +started: 2026-06-10 +--- + +## Goal + +`CodeGeneratorRequestWriter` lives in the `tool-base` module under +`io.spine.tools.code.proto`, with its spec, and the build is green. +This is the receiving half of +[base-libraries#938](https://github.com/SpineEventEngine/base-libraries/issues/938). + +## Context + +- The class is protoc-plugin tooling; its only consumers are the protoc-plugin + entry points of the Compiler and ProtoTap. Both already depend on ToolBase. +- Repackaged from `io.spine.code.proto` to `io.spine.tools.code.proto` to avoid + a split package across artifacts (see the issue discussion). +- `replaceExtension` is still consumed from `io.spine.io` (`base`); it moves to + ToolBase later under base-libraries#939. +- The removal half happens in `base-libraries` on the same-named branch. + +## Plan + +- [x] Add `CodeGeneratorRequestWriter.kt` to + `tool-base/src/main/kotlin/io/spine/tools/code/proto/`. +- [x] Add `CodeGeneratorRequestWriterSpec.kt` (with a local `constructRequest` + helper) to `tool-base/src/test/kotlin/io/spine/tools/code/proto/`. +- [x] Bump version `2.0.0-SNAPSHOT.398` -> `2.0.0-SNAPSHOT.399`. +- [ ] `./gradlew build` green; commit regenerated dependency reports if any. + - Blocked in the sandbox: all Spine artifact repositories return 403 for + the buildscript dependency `io.spine.tools:protobuf-setup-plugins`, so + no Gradle build can run here at all. Verification is delegated to PR CI. + - Local mitigations: the moved sources are unchanged from `base` (which + built and passed tests) except the package line, and every consumed API + (`replaceExtension`, `decodeBase64`, `toBase64Encoded`, + `extensionRegistry`, `KClass.parse`, `toJson`) was verified present in + `base` at the pinned version `2.0.0-SNAPSHOT.404`. +- [x] Push and open a draft PR. + +## Log + +- 2026-06-10 — drafted; executing autonomously per issue #938. +- 2026-06-10 — code added and version bumped; sandbox cannot resolve Spine + snapshot artifacts (403 on all repos), so the build runs on PR CI instead. diff --git a/.agents/tasks/archive/fix-build-cache-support.md b/.agents/tasks/archive/fix-build-cache-support.md new file mode 100644 index 000000000..ad7cba897 --- /dev/null +++ b/.agents/tasks/archive/fix-build-cache-support.md @@ -0,0 +1,85 @@ +--- +slug: fix-build-cache-support +branch: claude/busy-dirac-wwflbm +owner: claude +status: in-review +started: 2026-06-10 +--- + +## Goal + +Builds of projects using the `io.spine.generated-sources` and +`io.spine.descriptor-set-file` plugins succeed with `org.gradle.caching=true`: +after `./gradlew clean build`, the copied generated code under +`$projectDir/generated/` and the `desc.ref` file are present even +when `generateProto` is restored from the Gradle build cache. + +## Context + +With the build cache on, `clean build` fails in the `classic-codegen` module +(reproduced locally): `compileKotlin` reports `Unresolved reference 'Classpath'` +because no generated code is present. + +Root cause — same pattern as SpineEventEngine/compiler#67. Both plugins in +`protobuf-setup-plugins` do work in `doLast` of the cacheable +`GenerateProtoTask` without declaring outputs: + +- `GeneratedSourcePlugin` copies protoc output into + `$projectDir/generated//` (undeclared — the build-breaker: + `clean` deletes `generated/`, the task is restored `FROM-CACHE`, the + `doLast` copy never runs, compilation fails). +- `DescriptorSetFilePlugin` writes `desc.ref` next to the descriptor set file + (undeclared — missing from resources/JARs after a cache restore). + +The descriptor set file itself is already a declared output of +`GenerateProtoTask` (verified for protobuf-gradle-plugin 0.10.0 in +compiler#67), so only `desc.ref` needs declaring. + +This repository's own build applies the *published* +`protobuf-setup-plugins:2.0.0-SNAPSHOT.381` (dogfooding via the root +buildscript classpath), so `org.gradle.caching` stays disabled in +`gradle.properties` until a fixed version is published and the dogfooded +`ToolBase.version` is bumped (decision confirmed by the user). + +Known limitation (out of scope): in repos where the Spine Compiler launch +task also writes into `generated/` (e.g. `validation`), the +overlapping outputs degrade cacheability of the codegen tasks; correctness +is preserved because Gradle re-executes instead of restoring. + +## Plan + +- [x] Diagnose and reproduce on `classic-codegen` (`build` → `clean` → + `build` with `--build-cache`). +- [x] `GeneratedSourcePlugin`: declare `$projectDir/generated/` + as an output of `GenerateProtoTask`. +- [x] `DescriptorSetFilePlugin`: declare `desc.ref` + (`DescriptorSetReferenceFile.NAME`) as an output. +- [x] `gradle.properties`: keep `org.gradle.caching` commented out; explain + the dogfooding gate. +- [x] Integration tests in `protobuf-setup-plugins` (TestKit, per existing + spec style): `build` → `clean` → `build` with the cache ON (assert + `generateProto` is `FROM_CACHE`, generated sources + `desc.ref` + restored, compilation of code referencing a generated class succeeds) + and with the cache OFF (assert re-execution works as before). +- [x] Verify: new tests fail without the fix, pass with it; module build + green. + +## Log + +- 2026-06-10 — Investigated; reproduced on `classic-codegen` with the + dogfooded `.381` plugins; user chose to keep `org.gradle.caching` disabled + until dogfooding catches up. +- 2026-06-10 — The new integration test exposed a second gap: in a pure-Java + project `compileJava` does not depend on `generateProto` because + `configureSourceSetDirs()` severs the dependency carried by the protoc + output dirs and adds the `generated/` dirs as plain `File`s. (Masked in + Spine repos by the Kotlin compile dependency from `setupKotlinCompile()`.) + Fixed by adding the dirs via `project.files(dir).builtBy(task)`. +- 2026-06-10 — Verified: with the output declarations temporarily removed, + the cache-on test fails and the cache-off test passes; with the fix both + pass. `:protobuf-setup-plugins:build` and `dokkaGenerate` are green + (11 test suites, 0 failures). +- 2026-06-10 — Reviewed by `spine-code-review` and `kotlin-engineer` agents: + both approve. Applied follow-ups: `internal` on `BuildCacheSpec`, + explicit `this@configureSourceSetDirs` in `builtBy`, fixed stale + `copyGeneratedFiles` KDoc. Final build: 22 tests, 0 failures. diff --git a/.agents/tasks/archive/prohibit-automatic-commits.md b/.agents/tasks/archive/prohibit-automatic-commits.md deleted file mode 100644 index ff067c505..000000000 --- a/.agents/tasks/archive/prohibit-automatic-commits.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -slug: prohibit-automatic-commits -branch: prohibit-automatic-commits -owner: claude -status: in-review -started: 2026-05-20 ---- - -## Goal - -Make it a durable, team-wide rule that AI agents (Claude Code main thread, -every subagent, every skill) MUST NOT run `git commit` (or other -history-writing git/gh operations) unless authorization is *explicit and -current*. Authorization comes from one of two sources only: - -1. The currently active skill's `SKILL.md` contains an explicit - `## Commit authorization` section. -2. The user's current prompt explicitly instructs the operation - (e.g. "commit this", "push the branch"). - -Agents must otherwise stage changes and stop, letting the user review and -decide. This preserves today's auto-commit behavior for `bump-version` -and `bump-gradle`, which will declare authorization in their SKILL.md. - -## Context - -- Today's pain: Claude Code commits routinely, even when the user wants - to review diffs locally first. -- The project's `.claude/settings.json` already has `Bash(git commit:*)` - in `permissions.ask`. That asks the user per-commit but does not - redirect agent behavior — the agent still proposes commits constantly. - The fix is at the *instruction* layer, not the permission layer. -- Skills that legitimately commit today: `bump-version`, `bump-gradle`. -- Skills that do not commit but prescribe commit messages for the human: - `dependency-update` (already says "Do not commit. Do not push."). -- The user accepted removal of the global `~/.claude/settings.json` hook - added earlier this session. Enforcement lives in `.agents/` instructions - only. - -## Plan - -- [x] **1. Add the canonical rule to `.agents/safety-rules.md`.** - Added section *Commits and history-writing*. Lists default (no - history writes), two authorization sources, the fallback behavior - (stage + show diff + stop), and the operations covered. Names the - `## Commit authorization` marker. - -- [x] **2. Surface the rule in `.agents/quick-reference-card.md`.** - Added one-line pointer to `safety-rules.md` → *Commits and - history-writing*. - -- [x] **3. Add a workflow rule to `CLAUDE.md`.** - Added bullet under *Workflow Rules* referencing - `.agents/safety-rules.md`. - -- [x] **4. Declare authorization in `bump-version/SKILL.md`.** - Added a top-level `## Commit authorization` section above the - Checklist: exactly one commit, stage only `version.gradle.kts`, - subject `` Bump version -> `` ``, no push/tag/amend. - -- [x] **5. Declare authorization in `bump-gradle/SKILL.md`.** - Added a top-level `## Commit authorization` section above the - Checklist: up to two commits (wrapper + dependency reports), exact - subjects, no push/tag/amend. - -- [x] **6. Cross-check the non-authorizing skills.** - `dependency-update/SKILL.md` already explicit ("Do not commit. Do - not push.") — left as is. `pre-pr/SKILL.md` does not commit — left - as is. Other skills scanned (see Log). - -- [x] **7. Verification.** See Log entry — all three grep checks pass. - -## Out of scope - -- Project `.claude/settings.json` `ask` rule for `Bash(git commit:*)`: - leave as defense-in-depth (zero cost when the agent obeys the rule). -- `~/.claude/settings.json` global hook: already reverted earlier this - session per user direction. - -## Log - -- 2026-05-20 — drafted, awaiting plan approval. -- 2026-05-20 — approved by user. Executed steps 1–6. -- 2026-05-20 — verification: - - `grep -RIn '^## Commit authorization' .agents/skills/` returns exactly - `bump-gradle/SKILL.md` and `bump-version/SKILL.md` ✓ - - `safety-rules.md` referenced from `CLAUDE.md`, `quick-reference-card.md`, - `bump-version/SKILL.md`, `bump-gradle/SKILL.md` ✓ - - Literal `git commit` strings live only in the two authorizing skills ✓ - - `dependency-update/SKILL.md` still says "Do not commit. Do not push."; - `pre-pr/SKILL.md` still writes a sentinel and does not commit ✓ -- Status: `in-review` — awaiting user sign-off, then delete on merge to master. diff --git a/.agents/tasks/archive/prompt-caching-org.md b/.agents/tasks/archive/prompt-caching-org.md deleted file mode 100644 index 71f0c4fb9..000000000 --- a/.agents/tasks/archive/prompt-caching-org.md +++ /dev/null @@ -1,165 +0,0 @@ ---- -slug: prompt-caching-org -branch: improve-caching -owner: claude -status: in-review -started: 2026-05-24 -related-memories: [cache-warm-window, anthropic-api-caching] ---- - -## Goal - -Maximise Claude API prompt cache hit rates across the Spine GitHub organisation -(~40 sibling repos) so that repeated session starts and agent invocations read -from cache at 0.1× token cost rather than processing the full prompt fresh. - -## Context - -- Claude Code already applies automatic prompt caching to every API call it - makes. There is no single "enable" switch; the work is about raising the - cache hit rate and keeping it high. -- The `migrate` script overwrites `CLAUDE.md`, `.agents/`, `.claude/`, and - `buildSrc/` in each sibling repo with an exact copy from this repo. This - means all 40 repos hold byte-identical content after a `./config/pull` and - therefore share the same cache entry at any given config version. -- The `openai.yaml` files under each skill are FleetView UI interface metadata - only — they define display name and default prompt, not API call parameters. - `cache_control` cannot go there. -- No GitHub Actions workflow currently calls the Anthropic API directly. -- Current stable prefix: CLAUDE.md (≈ 900 tokens) + quick-reference card - (≈ 200 tokens) ≈ 1,100 tokens. - - This **clears** the 1,024-token minimum for Sonnet 4.6 / Opus. - - This **does not meet** the 4,096-token minimum for Haiku 4.5. -- The team memory system is empty; populating it will grow the stable prefix. -- Cache TTL defaults to 5 minutes. Sessions more than 5 minutes apart miss - the cross-session cache unless the extended 1-hour TTL is used. - -## Plan - -- [ ] **Step 0 — Diagnose why zero caching is happening and enable it** - - The Console Caching dashboard ("TeamDev Management OÜ", All workspaces) shows - no prompt caching in use — no `cache_control` blocks are being sent by any - caller. This is the highest-priority item; the remaining steps only add value - once caching is active. - - Sub-tasks: - - - **0a. Switch to Console OAuth on every developer machine** - - Raw API key auth loses per-developer identity (`email`, `orgId`, `orgName` - all null in `claude auth status`). Console OAuth preserves identity while - still billing to "TeamDev Management OÜ". - - **For each developer:** - 1. Remove `ANTHROPIC_API_KEY` from `~/.claude/settings.json` — it takes - precedence over OAuth in the auth stack and must be absent. - 2. Run `claude` → a browser window opens → log in with Console credentials - (the same account used at console.anthropic.com). - 3. Run `claude auth status` and confirm `email`, `orgId`, `orgName` are - populated. - - **For the org admin (Alexander):** - - Invite the second developer via Console → Settings → Members → Invite. - - Assign the "Developer" or "Claude Code" role. - - They accept the email invite, then follow the three steps above. - - - **0b. Enable 1-hour cache TTL on every developer machine** - - Console OAuth users get the **5-minute** default cache TTL — the 1-hour - TTL is only automatic for claude.ai subscription users. Add the opt-in - to `~/.claude/settings.json` on every developer machine: - - ```json - { - "env": { - "ENABLE_PROMPT_CACHING_1H": "1" - } - } - ``` - - Restart Claude Code after saving. This is the highest-impact change in - the entire plan — without it, cache entries expire every 5 minutes and - cross-session hits are rare. - - - **0c. Verify caching is active** — start a Claude Code session, make a - few turns, wait 2–3 minutes, then check Analytics → Usage in the Console - under "TeamDev Management OÜ". Non-zero `cache_creation_input_tokens` - confirms caching is active. Non-zero `cache_read_input_tokens` on a - subsequent session in the same hour confirms hits are occurring. - - - **0d. Investigate remote skill calls** — FleetView-managed remote skills - (the 7 skills with `openai.yaml`) make their own API calls through the - agent platform. Confirm whether those calls include `cache_control`; if - not, this may require configuration in the FleetView platform outside - this repo. - - Until steps 0a–0b are done on both developer machines, Steps 1–3 improve - future cache hygiene but produce limited cost savings. - -- ~~**Step 1 — Cache-hygiene team memory**~~ — *reverted 2026-05-25: the - batching guidance was too restrictive on `config` changes; removed - `.agents/memory/feedback/cache-hygiene.md` and its references.* - -- [x] **Step 2 — Post-migration cache-warm window (reference memory)** - - Create `.agents/memory/reference/cache-warm-window.md` documenting: - - Realistic concurrency is 1–2 developers working on different repos at the - same time, not the full fleet of 40. - - Default TTL is 5 minutes. If a second session starts within 5 minutes of - the first (on the same config version), it hits the warm entry rather than - writing a new one. - - Extended 1-hour TTL (available in direct API calls via - `cache_control: {ttl: "1h"}`) gives a wider window, at 2× write cost per - token — still profitable after even one hit within the hour. - - Update `.agents/memory/MEMORY.md` index. - -- [x] **Step 3 — API caching pattern reference memory (for future direct calls)** - - No workflow currently calls the Anthropic API directly, but when one is - added, developers need the pattern immediately. - - Create `.agents/memory/reference/anthropic-api-caching.md` documenting: - - Use `cache_control: {type: ephemeral}` on the system message block for - 5-minute TTL (1.25× write / 0.1× read). - - Use `cache_control: {type: ephemeral, ttl: "1h"}` for 1-hour TTL - (2× write / 0.1× read) — right for any workflow job spaced > 5 min apart. - - Place stable content (system instructions, skill definitions, shared - context) **before** any dynamic per-request content so the breakpoint - sits at the end of the stable prefix. - - Monitor: `usage.cache_read_input_tokens` should grow relative to - `usage.cache_creation_input_tokens` as the cache warms. - - Future: once direct API calls exist, consider a cache pre-warm job - triggered on push to `master` — calls the API with `max_tokens: 0` and - `cache_control: {ttl: "1h"}` so the first session after a config change - hits rather than writes. - - Update `.agents/memory/MEMORY.md` index. - -- [x] **Step 4 — API workspace consolidation (already confirmed — verify stays true)** - - A cache entry is visible only to API calls made with a key from the **same - Anthropic workspace** (a named sub-group within your Anthropic Console - organisation). Two requests using keys from different workspaces never share - cache, even if they send identical prompts. - - **Current state (confirmed):** "TeamDev Management OÜ" has a single default - workspace (Environments list is empty). Both developers use Console API keys - from this organisation. Both developers share the same cache pool — no action - needed today. - - **Keep true as the team grows:** do not create separate Environments per - developer or per project unless cache isolation is intentional. Any new API - key issued for a new caller (GitHub Actions, scripts, new developer) should - be issued from the same workspace. - -## Log - -- 2026-05-24 — drafted from codebase audit; awaiting review and approval -- 2026-05-24 — revised per review: added buildSrc to migrate list, removed dependency-audit caching step, corrected concurrency description to 1–2 repos, dropped pre-warm workflow step (pattern preserved in Step 3 memory), clarified per-workspace semantics in Step 4 -- 2026-05-24 — added Step 0 after Console Caching dashboard confirmed zero prompt caching in use; workspace confirmed as single default (no Environments), both devs on same org — Step 4 updated to reflect confirmed state -- 2026-05-24 — Step 0 revised: root cause identified — Console API key users get 5-min TTL by default vs 1-hour for subscription users; ENABLE_PROMPT_CACHING_1H=1 is the fix; warning on first launch is one-time approval only -- 2026-05-24 — Step 0 revised again: switched to Console OAuth (not raw API key) to preserve per-developer identity; ENABLE_PROMPT_CACHING_1H=1 still required for Console OAuth users (5-min TTL default applies to all non-subscription auth) -- 2026-05-24 — Steps 1–4 complete: three memory files created, MEMORY.md index updated, workspace consolidation confirmed; Step 0 remains in progress (Console OAuth setup and verification) -- 2026-05-25 — reverted Step 1: removed `cache-hygiene.md` and references — batching guidance was too restrictive for `config` development cadence diff --git a/.agents/tasks/archive/raise-coverage-kover-migration.md b/.agents/tasks/archive/raise-coverage-kover-migration.md deleted file mode 100644 index 268ba0780..000000000 --- a/.agents/tasks/archive/raise-coverage-kover-migration.md +++ /dev/null @@ -1,449 +0,0 @@ ---- -slug: raise-coverage-kover-migration -branch: coverage-tests-skill -owner: claude -status: in-review -started: 2026-05-30 ---- - -## Goal - -Extend the `raise-coverage` skill with a precondition step that migrates a -consumer repo from the vanilla JaCoCo Gradle plugin to JetBrains Kover (with -`useJacoco(version = Jacoco.version)` so the engine and the JaCoCo-format XML -remain unchanged). The skill becomes **Kover-only** post-migration. Adjacent -JaCoCo-distribution code in `config`'s `buildSrc` is marked deprecated with a -pointer to the Kover path; no behaviour change for repos that still consume -the deprecated script plugins. - -## Context - -The skill in `.agents/skills/raise-coverage/` currently supports two coverage -frontends: Kover (consumer repos) and raw JaCoCo (the `config` repo itself). -Per user decision, the skill collapses to Kover-only and gains a Step 0 that -detects vanilla JaCoCo, proposes a one-shot repo-wide migration, waits for -approval, applies it, smoke-checks, and only then resumes the normal flow. - -`config` itself has no production Kotlin/Java code, so the skill never runs -*on* `config`. However, `config` distributes vanilla-JaCoCo infrastructure -(`buildSrc/src/main/kotlin/jacoco-kotlin-jvm.gradle.kts`, `jacoco-kmm-jvm.gradle.kts`, -and the `JacocoConfig` helper) that consumer repos may still apply — these -get deprecation annotations + a runtime `logger.warn` (script plugins only) -but stay on disk so existing consumers keep building. `Jacoco.kt` (engine -version) and `Kover.kt` (plugin version) are unchanged because Kover uses -`useJacoco(version = Jacoco.version)`. - -### Decisions (locked — do not re-litigate) - -| # | Decision | -|---|----------| -| 1 | Invocation: implicit precondition (Step 0 of the skill) | -| 2 | Scope: repo-wide, proposed once | -| 3 | Trigger: always, unless Kover is already applied everywhere | -| 4 | Both plugins applied: always remove `jacoco`, keep Kover | -| 5 | KMP: JVM-target-only migration via Spine's `kmp-module` script plugin — uses the same `::koverXmlReport` task and `build/reports/kover/report.xml` path as Kotlin-JVM, because `kmp-module` configures only Kover's `total` report (no named variants, so no `koverXmlReport` task is generated) | -| 6 | CI / `.codecov.yml` / scripts: all updated to Kover paths and tasks | -| 7 | Plugin/engine version: reference `io.spine.dependency.test.Kover` / `Jacoco`; do not hardcode | -| 8 | Translation fidelity: best-effort full; flag unmappable constructs | -| 9 | Post-migration: skill flow is Kover-only | -| 10 | No-coverage case: silent install, no approval gate | -| 11 | Verification: smoke check only (`koverXmlReport` exists + parses) | -| 12 | `JacocoConfig` and `jacoco-*.gradle.kts`: mark deprecated, do not delete | - -### Verified facts (from Phase 1 exploration) - -- `buildSrc/src/main/kotlin/jvm-module.gradle.kts:54` applies Kover; `:99` - configures it with `useJacoco(version = Jacoco.version)` and XML-on-check. -- `buildSrc/src/main/kotlin/kmp-module.gradle.kts:74` and `:181` mirror the - above for KMP modules. -- `buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/` contains the - JaCoCo-pipeline classes to be deprecated: `JacocoConfig`, `TaskName`, - `CodebaseFilter`, `FileFilter`, `FileExtensions`, `FileExtension`, - `PathMarker`. -- `scripts/upload-artifacts.sh:38` references `build/jacoco*` directories. -- `.github/workflows/*.yml` in `config` contain no JaCoCo references. - -## Implementation - -Paths are relative to the `config` repo root. - -### Area A — Skill files - -#### A1. `.agents/skills/raise-coverage/SKILL.md` - -- Frontmatter `description: >`: drop "Kover or JaCoCo frontend"; phrase the - report as "Kover's JaCoCo-format XML report"; add: "Before anything else, - ensures the repo is on Kover — if vanilla JaCoCo is detected, proposes a - one-shot repo-wide migration and waits for approval." -- Body "with JaCoCo" → "with **Kover**'s JaCoCo-format XML report". -- **Scope** bullet rewritten Kover-only. Per-module task - `::koverXmlReport`; XML at - `/build/reports/kover/report.xml`. Same task and path on KMP - modules configured by Spine's `kmp-module` script plugin — it sets up only - Kover's `total` report (no named variants, so no - `koverXmlReport` task is generated). Strip every "raw JaCoCo" / - `jacocoTestReport` / `jacocoRootReport` reference as a normal mode. -- **Insert new `## Step 0 — Ensure Kover`** between `## Inputs` and - `## Workflow`. Three branches: - 1. Kover applied everywhere → silently proceed. - 2. Nothing applied anywhere → silently install Kover; record - "Migration: installed Kover" in the final Report; no approval gate. - 3. Vanilla JaCoCo in ≥1 module → emit a proposal and **wait for approval**. -- **Proposal output structure** (Markdown, in this order): - 1. **Detected** — every module applying `jacoco` / `JacocoPlugin` / - `JacocoConfig.applyTo` / a `jacoco-*.gradle.kts`; annotate "vanilla - only" vs. "JaCoCo+Kover both"; note any root `jacocoRootReport`. - 2. **Plan** — file edits with paths (per-module `build.gradle.kts`, root - `build.gradle.kts`, `.codecov.yml`, `.github/workflows/*.yml`, - `scripts/*.sh`). - 3. **Translation notes** — applicable rows from the migrate-to-kover table. - 4. **Manual-review surfaces** — items the user must decide on. - 5. **Smoke check that will follow** — the E1 commands. - 6. Close with: "Confirm to apply, or call out anything to change first." -- **Wait for approval.** No writes until explicit "go" / "yes" / "apply". - On adjustment requests, regenerate the proposal and wait again. -- **Apply** per `references/migrate-to-kover.md`; log `edited ` per - file. Any unresolved manual-review surface → stop ("needs your call on - ``"). -- **Smoke check** per E1. Failure → stop; do not fall through. -- **Resume** at Workflow step 1. -- **Workflow step 1** (`--triage`): "per-module `koverXmlReport`, or the - aggregate `jacocoRootReport` in `config`" → "per-module `koverXmlReport`, - or the root-level Kover aggregation task `koverXmlReport` if the repo - wires one". -- **Workflow step 2**: "Detect the coverage frontend and run …" → "Run - `::koverXmlReport`" — same task on JVM and KMP modules configured - by Spine's `kmp-module` script plugin (no named variants → no - `koverXmlReport` task). Drop "either way" from the XML-parsing - sentence. -- **Workflow step 6**: "(`koverXmlReport` or `jacocoTestReport`)" → - `::koverXmlReport`. -- **Report**: add a **Migration** section (emitted only when Step 0 did work). -- **Safety**: add a bullet — "No migration without explicit approval when - vanilla JaCoCo is detected. Silent install only when *no* frontend is in - place." - -#### A2. `.agents/skills/raise-coverage/references/coverage-signals.md` - -- Top blurb: drop the "two frontends" paragraph; replace with a single - paragraph stating that the engine is JaCoCo and the Spine convention is - Kover with `useJacoco(version = Jacoco.version)`; the XML is - JaCoCo-format either way. -- Delete the entire **"Two coverage frontends"** section (current lines - 11–57). Replace with **"Where the report lives"** — per-module task / XML - path, root-level aggregation paths, `find` recipe if unknown. -- **"Generating a report"**: drop the two JaCoCo `./gradlew` lines; keep - Kover. Same task on KMP modules configured by Spine's `kmp-module` script - plugin — no `koverXmlReport` task is generated unless a named - `variant("…") { … }` block is declared. -- **"Extracting gaps for a class"**: drop "or the jacoco path". -- **"KMP / Kotlin-JVM modules"**: keep first sentence; delete the second - sentence about `jacoco-*-jvm` exec data paths. -- **Verification**: "the **same** report task" → `::koverXmlReport`. -- **Codecov triage tier appendix**: keep verbatim. - -#### A3. `.agents/skills/raise-coverage/references/migrate-to-kover.md` (new) - -Eight sections: - -1. **Purpose** — one paragraph; link to - and the - `migrations/migration-to-0.7.0.html` migration guide. -2. **Detection signals** — grep patterns per module's `build.gradle.kts`: - - Plugin block: `^\s*jacoco\b` inside `plugins {`, `id\("jacoco"\)`, - `apply\(\)`, `apply\(plugin = "jacoco"\)`. - - Script plugin: `apply\(plugin = "jacoco-`. Covers `jacoco-kotlin-jvm`, - `jacoco-kmm-jvm`. - - `JacocoConfig`: `JacocoConfig\.applyTo` or imports of - `io.spine.gradle.report.coverage.JacocoConfig`. - - DSL: `jacoco\s*\{`, `jacocoTestReport\s*\{`, - `jacocoTestCoverageVerification\s*\{`, `tasks\.named\("jacoco`. - - Kover applied: `org.jetbrains.kotlinx.kover`, or `id("jvm-module")` / - `id("kmp-module")` (both auto-apply Kover). - - Root aggregation: `jacocoRootReport`. - - Multi-module walk: parse `settings.gradle.kts` for `include(...)`; - honor `project(":x").projectDir = file(...)` overrides. -3. **Per-module migration**: - - Add Kover via `id(Kover.id)` if `buildSrc` is on the classpath; - otherwise `id("org.jetbrains.kotlinx.kover") version ""`. - If `jvm-module` / `kmp-module` is applied, skip the add (log "already - via jvm-module"). - - Strip `jacoco` from `plugins { }`. - - **Translation table**: - | JaCoCo construct | Kover / action | - |---|---| - | `jacoco { toolVersion = Jacoco.version }` | drop (engine version → root `useJacoco(...)`) | - | `jacoco { toolVersion = }` | **flag** | - | `reports { xml=true; html=true; csv=false }` | `kover { reports { total { xml { onCheck.set(true) }; html { } } } }` | - | `executionData.setFrom(...)` | **flag** (Kover-managed) | - | `sourceDirectories.setFrom(...)` | **flag** (Kover-inferred) | - | `classDirectories.setFrom(...)` (Kotlin-JVM/KMP `walkBottomUp`) | drop; **flag** if non-Kotlin | - | `reports.xml.outputLocation.set(...)` | **flag** (fixed path) | - | `tasks.named("jacocoTestReport") { dependsOn(...) }` | rewrite to `tasks.named("koverXmlReport")` | - | `violationRules { rule { limit { counter; value; minimum } } }` | `kover { reports { verify { rule { … } } } }`; counter map: INSTRUCTION/BRANCH/LINE = same; METHOD → INSTRUCTION + flag; CLASS → flag | - - `jvm-module` / `kmp-module` simplification: Kover already there; - migration becomes "remove JaCoCo bits only". -4. **Root-level aggregation**. Trigger: source had `jacocoRootReport` **or** - >1 module to aggregate. - - Apply Kover at root (skip if root applies `jvm-module`). - - `dependencies { kover(project(":foo")); … }` per consuming module. - - `kover { useJacoco(version = Jacoco.version); reports { total { xml { onCheck.set(true) }; html { } } } }`. - - Mirror per-module `violationRules` to root `verify { rule { … } }` - only if the source repo had a root-level rollup. -5. **CI / `.codecov.yml` / scripts** — substitutions: - - Workflows: `jacocoTestReport` → `koverXmlReport`; `jacocoRootReport` → - root `koverXmlReport`; - `build/reports/jacoco/test/jacocoTestReport.xml` and - `build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml` → - `build/reports/kover/report.xml`. - - `.codecov.yml`: same path tokens; preserve `ignore` and - `coverage.status` verbatim. - - `scripts/*.sh`: `build/jacoco*` glob → `build/reports/kover`; **flag** - scripts reading raw `.exec` paths (e.g., - `scripts/upload-artifacts.sh:38` in `config`). -6. **KMP recipe**. JVM-only target. Same task and XML path as Kotlin-JVM: - `::koverXmlReport` and `/build/reports/kover/report.xml`. - Spine's `kmp-module` script plugin configures only Kover's `total` report, - so no `koverXmlReport` task is generated — CI / `.codecov.yml` - must reference the unsuffixed path. A `koverXmlReportJvm` task would only - appear if a named `variant("jvm") { … }` block were declared, which - `kmp-module` does not do. -7. **Manual-review surfaces** (flag and ask): - - Custom `sourceDirectories` / `classDirectories` on `jacocoTestReport` - (the `jacoco-*-jvm.gradle.kts` pattern). - - Custom `reports.xml.destination` / `outputLocation`. - - Custom `executionData` paths. - - Indirect `jacoco.toolVersion` (property files, `gradle.properties`). - - Multi-pipeline setups where both reports are intentional. - - `JacocoConfig.applyTo(rootProject)` outside `config`. - - Custom convention plugins applying JaCoCo under a non-`jacoco-…` name. - - Non-JVM KMP targets — out of scope (decision 5). -8. **References** — links to the Kover migration guide and DSL docs. - -#### A4. `.agents/skills/raise-coverage/agents/openai.yaml` - -- `short_description`: "Migrate to Kover if needed, then generate unit tests - to close coverage gaps." -- `default_prompt`: rewrite to name Step 0 — detect setup; if vanilla - JaCoCo found, propose a one-shot repo-wide migration and wait for - approval; if nothing applied, install Kover silently. After smoke check, - run the existing flow (localize from Kover XML; propose; approve; - generate Kotest / Truth stubs; re-verify). End with: "Tests-only changes - do not require a version bump." - -### Area B — BuildSrc deprecations in `config` - -#### B1. `buildSrc/src/main/kotlin/jacoco-kotlin-jvm.gradle.kts` and `jacoco-kmm-jvm.gradle.kts` - -Runtime behaviour unchanged. Two edits per file: - -1. Below the copyright header, insert a `// DEPRECATED:` block: "This - script plugin distributes vanilla JaCoCo. New code should apply - `jvm-module` (or `kmp-module`), which configures Kover via - `useJacoco(version = Jacoco.version)`. The `raise-coverage` skill - migrates existing consumers. Kept so older consumer repos continue to - build; will be removed in a future release." -2. Immediately before `plugins { jacoco }`, add - `logger.warn("'jacoco-kotlin-jvm' is deprecated; use 'jvm-module' which applies Kover. See .agents/skills/raise-coverage/references/migrate-to-kover.md.")` - (use `jacoco-kmm-jvm` and `kmp-module` in the KMM file). - -#### B2. `@Deprecated` on JaCoCo-pipeline classes under `buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/` - -All `DeprecationLevel.WARNING`, no `ReplaceWith` (the replacement is a -multi-block DSL). - -- `JacocoConfig.kt` — class `JacocoConfig`: "Use Kover's root-level - aggregation (`dependencies { kover(project(...)) }` plus - `kover { useJacoco(version = Jacoco.version); reports { total { xml { onCheck = true } } } }`) - instead of `JacocoConfig.applyTo(...)`. The `raise-coverage` skill - performs this migration automatically." -- `TaskName.kt` — enum: "Internal task-name catalog for the deprecated - `JacocoConfig` pipeline; Kover uses its built-in task names - (`koverXmlReport`, etc.). Removed when `JacocoConfig` is." -- `CodebaseFilter.kt` — class: "Used only by the deprecated `JacocoConfig`. - Kover infers source sets and respects `kover { filters { excludes { … } } }`." -- `FileFilter.kt` — object: same wording as `CodebaseFilter`. -- `FileExtensions.kt` — `@file:Deprecated("Path/extension helpers used only by the deprecated `JacocoConfig` pipeline. Removed when `JacocoConfig` is.")`. -- `PathMarker.kt` enum and `FileExtension.kt` enum — same wording as - `FileExtensions`. - -For in-package self-references, accept the warnings or apply -`@Suppress("DEPRECATION")` on the call sites. - -#### B3. Untouched - -`buildSrc/src/main/kotlin/io/spine/dependency/test/Jacoco.kt` and `Kover.kt` -remain unchanged. `Jacoco.kt` is the engine-version source used by -`kover { useJacoco(version = Jacoco.version) }`. - -### Area C — Adjacent docs in `config` - -#### C1. `.agents/tasks/raise-coverage.md` - -- Decisions table "Coverage engine" row: "exposed via Kover - (`koverXmlReport`); when a consumer repo still has vanilla JaCoCo, the - skill migrates it first; Codecov deferred." -- "Verified facts": replace the **JaCoCo paths** bullet with a Kover-paths - bullet (`/build/reports/kover/report.xml` — same on KMP via - `kmp-module`, which configures only the `total` report; root aggregation - `build/reports/kover/report.xml`; Kover manages exec data internally). -- Plan: append `- [ ]` "Kover-only pivot: implement - `.agents/tasks/raise-coverage-kover-migration.md`". -- Log: append a 2026-05-30 entry summarising the pivot. -- Keep `status: draft`. - -#### C2. `.claude/commands/raise-coverage.md` - -- Description: "Ensure the repo is on Kover (migrate from JaCoCo if - needed), then localize coverage gaps and generate missing unit tests for - a module or path." -- Order bullet: `::jacocoTestReport` → `::koverXmlReport`. - `--triage` line: "ranked JaCoCo gap report" → "ranked Kover gap report". -- After the Skill bullet, add: "First-time setup: the skill enforces - Kover. If vanilla JaCoCo is found anywhere, the skill proposes a - repo-wide migration and **waits for your approval**. See - `references/migrate-to-kover.md`." -- `allowed-tools`: unchanged. - -#### C3. Other docs - -- `.agents/_TOC.md` — no edit. -- `.agents/tasks/buildsrc-gradle-review-findings.md` — above each item - referencing `jacoco-kmm-jvm.gradle.kts` or `JacocoConfig.kt` (lines - 77–78, 101–103, 121–122, 169–172, 201–203), insert one line: - "**Superseded by Kover-only migration**: these files are deprecated; do - not invest in micro-rewrites. See - `.agents/skills/raise-coverage/references/migrate-to-kover.md`." -- `scripts/upload-artifacts.sh` — add `# DEPRECATED:` comment line above - line 38 (`JACOCO_REPORTS=…`) pointing at the migration reference. No - behaviour change. -- `scripts/buildSrc-migration.kts`, `migrate`, `lychee.toml` — no edits. - -### Area E — Verification - -#### E1. Step 0 smoke check (post-migration) - -1. Run `./gradlew ::koverXmlReport --quiet` on the smallest leaf - JVM migrated module; if the root was touched, also `./gradlew - koverXmlReport --quiet`. -2. Assert `/build/reports/kover/report.xml` exists, is non-empty, - and the first non-XML-declaration line contains `Kt` synthetic) and emits both `FQN` and - `FQN$*` exclusion patterns to cover nested classes. The skill's - migration step now rewrites `JacocoConfig.applyTo` → `KoverConfig.applyTo` - instead of deleting the call. `./gradlew -p buildSrc compileKotlin` - green. diff --git a/.agents/tasks/raise-coverage-tool-base.md b/.agents/tasks/archive/raise-coverage-tool-base.md similarity index 100% rename from .agents/tasks/raise-coverage-tool-base.md rename to .agents/tasks/archive/raise-coverage-tool-base.md diff --git a/.agents/tasks/archive/raise-coverage.md b/.agents/tasks/archive/raise-coverage.md deleted file mode 100644 index b0fc517ef..000000000 --- a/.agents/tasks/archive/raise-coverage.md +++ /dev/null @@ -1,283 +0,0 @@ ---- -slug: raise-coverage -branch: coverage-tests-skill -owner: claude -status: draft -started: 2026-05-29 -updated: 2026-05-30 ---- - -## Goal - -Stand up a reusable, Spine-native agent skill — **`raise-coverage`** — that raises -JVM test coverage by localizing uncovered lines/branches with JaCoCo and -generating policy-compliant unit tests. The skill lives in `config` and -propagates to all ~50 repos via `./config/pull`. Success = the skill and its -wrappers are authored, distribution is wired, and the full loop has been -dry-run on one `base-libraries` module locally (nothing committed). - -## Context - -Scoped in Claude Chat; the produced `SKILL.md` was lost and the two surviving -files (`coverage-signals.md`, `coverage-tests.md`) are **drafts** to be -rewritten, not shipped. Clarification produced eight decisions that narrow and -simplify the original draft. - -### Decisions (locked — do not re-litigate) - -| Decision | Choice | -|---|---| -| Skill name | **`raise-coverage`** (verb-noun, like `write-docs` / `bump-version`) | -| Workflow | localize → propose cases → **wait for approval** → generate → verify; plus read-only `--triage` | -| Coverage engine | **JaCoCo engine**, exposed via **Kover** (`koverXmlReport`); when a consumer repo still has vanilla JaCoCo, the skill migrates it first (see `raise-coverage-kover-migration.md`); Codecov deferred | -| Test language | **Kotlin + Kotest** for every new test, regardless of the language of the code under test; class names use the **`Spec`** suffix (e.g. `AbstractSourceFileSpec`). Truth proto extension is reachable only when Kotest cannot express the assertion | -| Scratch dir | reuse existing `tmp/` → `tmp/base-libraries` (already gitignored via `/tmp`) | -| Done bar | full loop on one `base-libraries` module, **local, nothing committed** | -| Codex parity | include `agents/openai.yaml` | -| Branch/task | keep branch `coverage-tests-skill`; `git mv` task file → `raise-coverage.md` | - -### Verified facts (baked into the deliverables) - -- **Skill system**: source of truth is `.agents/skills//`; `.claude/skills` - is a **symlink** to `../.agents/skills` (author once). `.claude/commands/.md` - is the slash-command wrapper. Action skills also ship `agents/openai.yaml`. -- **Distribution**: `migrate` (sourced by `pull`) copies the whole `.agents` + - `.claude` tree, so a new skill auto-propagates — **except** a Hugo-only-repo - prune block that strips JVM-specific skills. `raise-coverage` is JVM-specific - and must be added to that block. -- **Test stack**: Kotest `6.1.11` (`io.kotest:kotest-assertions-core`), Google - Truth `1.4.4` (`truth` + `truth-proto-extension`), JUnit `6.0.3` - (`org.junit:junit-bom`), JaCoCo `0.8.14` (`Jacoco.kt`, already bumped in the - working tree). -- **House test idiom**: JUnit Jupiter structure (`@Test` / `@Nested` / - `@DisplayName` / `@TempDir`) + **Kotest matchers** (`shouldBe`, `shouldThrow`, - `shouldContainExactlyInAnyOrder`). NOT pure Kotest specs. (Verified in - `buildSrc/src/test/.../FileExtensionsTest.kt`.) -- **Kover paths** (post-migration): per-module - `/build/reports/kover/report.xml` — same on Kotlin-JVM and KMP - modules configured by Spine's `kmp-module` script plugin, which sets up - only the `total` Kover report (no named variants, so no - `koverXmlReport` task is generated). Root aggregation (when wired): - `build/reports/kover/report.xml`. Kover manages exec data internally — no - raw `.exec` paths are exposed to consumers. -- **Runtime successor to `JacocoConfig`**: - `io.spine.gradle.report.coverage.KoverConfig.applyTo(rootProject)`. Applies - the Kover plugin at the root, wires `dependencies { kover(project(...)) }` - for every Kover-enabled subproject, pins - `useJacoco(version = Jacoco.version)`, and pushes the union of generated-class - FQNs as Kover excludes into both per-module and root reports. Preserves the - generated-code-filtering behavior previously provided by `CodebaseFilter` so - the skill does not hallucinate gaps for generated classes. -- **Never test**: generated code (any path containing `generated`), `examples`, - existing `test` sources. `.codecov.yml` scope is `src/main/**` only. -- **No version bump** for tests-only changes (contrast with other action skills, - which end by invoking `/version-bumped`). - -## Deliverables (file-by-file) - -1. **`.agents/skills/raise-coverage/SKILL.md`** — frontmatter `name` + - `description: >`. Sections: Goal/scope (**Kover-only**, using its - JaCoCo-format XML report; human `src/main` only) · Inputs (`$ARGUMENTS` - = `:module` | path | `--triage`) · Step 0 — Ensure Kover (read-only - under `--triage`; silent install when no coverage plugin is in place; - **propose-and-wait** when vanilla JaCoCo is detected, per - `references/migrate-to-kover.md`) · Workflow (1 resolve target → - 2 localize gaps from Kover's `report.xml` → 3 read code-under-test + - existing tests + collaborators → 4 **propose test-case list and WAIT**; - `--triage` stops here with the ranked report → 5 generate → 6 verify) · - Test-generation rules (stubs not mocks; **Kotlin + Kotest** universal — - JUnit Jupiter structure with Kotest assertions, written in Kotlin even - when the code under test is Java; class names use the **`Spec`** suffix; - `truth-proto-extension` is reachable only as an isolated fallback for - Protobuf assertions Kotest cannot express; cover edge cases; scaffold - `when`/sealed branches; skip generated/excluded paths) · Report format · - Safety (`--triage` is read-only; migration requires approval when - vanilla JaCoCo is detected; never weaken a `.codecov.yml` target; never - add a mocking dependency; read-only until step-4 approval; no version - bump for tests-only). - -2. **`.agents/skills/raise-coverage/references/coverage-signals.md`** — JaCoCo - mechanics (rewritten from the draft, corrected to this repo): per-module vs - `jacocoRootReport`; the real report paths above; XML structure and gap rules - (`ci==0` uncovered line, `mb>0` partial branch); `xmllint`/Python extraction - recipes; the generated-code exclusion; `.codecov.yml` scope; KMP - source-set/exec-data variants. Ends with a short **"Future: Codecov triage - tier"** appendix capturing the deferred two-tier design. - -3. **`.agents/skills/raise-coverage/agents/openai.yaml`** — Codex parity - (`interface.display_name` / `short_description` / `default_prompt`). - -4. **`.claude/commands/raise-coverage.md`** — thin slash-command wrapper. - `allowed-tools: Read, Edit, Write, Grep, Glob, Bash(./gradlew:*), - Bash(git status:*), Bash(find:*)` (dropped `curl`/`WebFetch` — Codecov - deferred). Body points at the skill, states the order, honors `testing.md` + - `coding-guidelines.md`, notes no version bump for tests-only. - -5. **`.agents/_TOC.md`** — add `23. [Raise test coverage](skills/raise-coverage/SKILL.md)`. - -6. **`migrate`** — add `raise-coverage` to the Hugo-only prune block: - `rm -rf ../.agents/skills/raise-coverage`, - `rm -rf ../.claude/skills/raise-coverage`, - `rm -f ../.claude/commands/raise-coverage.md` (mirrors existing JVM-skill entries). - -7. **`.agents/tasks/raise-coverage.md`** — this file (`git mv` from - `improve-test-coverage.md`). - -> Dropped from the original plan: the standalone "install README". We author -> directly in `config`, so there is no separate install step — placement is -> documented here. - -## Reusable test harness - -```bash -# from the config repo root -mkdir -p tmp -git clone --recurse-submodules https://github.com/SpineEventEngine/base-libraries tmp/base-libraries -cd tmp/base-libraries && git submodule update --init --recursive - -# lay down the published .agents/.claude baseline -./config/pull - -# overlay the in-development skill on top of the baseline -cp -R /.agents/skills/raise-coverage .agents/skills/ -cp /.claude/commands/raise-coverage.md .claude/commands/ -# (.claude/skills → ../.agents/skills symlink resolves the skill automatically) -``` - -`pull` fetches the **published** config from master, so it won't contain -`raise-coverage` yet — the overlay-copy injects the in-dev version. Then execute -the skill's procedure against one module and verify. - -## Plan - -- [x] Housekeeping: `git mv` task file → `raise-coverage.md`; `TaskCreate` to track. -- [x] Author `SKILL.md`, `references/coverage-signals.md`, `agents/openai.yaml`, - and the `.claude/commands/raise-coverage.md` wrapper. -- [x] Wire-up: add `_TOC.md` entry; add `raise-coverage` to the `migrate` prune block. -- [x] Harness + pilot: cloned `base-libraries` into `tmp/`, ran `./config/pull`, - overlaid the skill. Localized gaps via Kover (`koverXmlReport`), then closed - `EnvironmentType.equals()`/`hashCode()` with a Java+Truth test — the gap went - to zero (4 tests green, nothing committed). Hardened `SKILL.md` + - `coverage-signals.md` for the Kover frontend and for non-actionable - (inline / unreachable) gaps surfaced by the pilot. -- [x] Review: `review-docs` over the new Markdown; sanity-check the `migrate` - edit; confirm nothing staged in `tmp/base-libraries`. -- [x] Kover-only pivot: implemented `.agents/tasks/raise-coverage-kover-migration.md`. - Dropped dual-frontend logic, added Step 0 migration gate to `SKILL.md`, - and deprecated the JaCoCo-pipeline `buildSrc` helpers (`JacocoConfig`, - `CodebaseFilter`, `FileFilter`, `FileExtensions`, `FileExtension`, - `PathMarker`, `TaskName`, plus the `jacoco-*-jvm.gradle.kts` script - plugins). Authored `KoverConfig.kt` as the live runtime successor — - preserves the generated-code exclusion previously provided by - `CodebaseFilter` and wires per-subproject `kover(project(...))` - aggregation, with the union of generated FQNs pushed into both - per-module and root reports. Three review/fix cycles - (`kotlin-review` + `review-docs`, parallel) all returned APPROVE; - `./gradlew -p buildSrc compileKotlin` green. -- [x] Re-run pilot with the updated skill against `tmp/base-libraries` - (`--triage` first, then close one fresh gap). Step 0 correctly detected - the hybrid state (root vanilla JaCoCo + subprojects already on Kover via - `module` script plugin), emitted the structured proposal, and on approval - migrated the root build (drop `jacoco` plugin; swap `JacocoConfig` → - `KoverConfig`; lift the call out of `gradle.projectsEvaluated`). Smoke - check passed: `:base:koverXmlReport` and root `koverXmlReport` both - produce JaCoCo-format XML with ``; generated - `OptionsProto` correctly excluded. Closed gaps in - `io.spine.code.fs.AbstractSourceFile` with a Java + Truth + `@TempDir` - stub (6 cases): coverage delta 20 missed LINE → 2, 2 missed BRANCH → 0 - (LINE 91 %, BRANCH 100 %). The residual 2 missed LINE are non-actionable - (`throw newIllegalStateException(...)` where the helper throws - internally) — pattern added to `references/coverage-signals.md`. -- [ ] On merge: flip `status: done` and delete this task file per the - `.agents/tasks/` lifecycle. - -## Verification (the done bar) - -- Files: `_TOC.md` resolves; `.claude/skills/raise-coverage/SKILL.md` resolves - through the symlink; `openai.yaml` parses. -- Distribution: confirm the `migrate` prune block lists `raise-coverage` so - Hugo-only repos won't receive it. -- End-to-end ✅: in `tmp/base-libraries`, `:environment:koverXmlReport` ran; - `EnvironmentTypeTest` (Java + Truth, 4 tests) compiles and passes; re-parsing - `build/reports/kover/report.xml` shows `EnvironmentType` `missedLINE`/ - `missedBRANCH` → 0 (was 2 / 1). **Nothing committed to base-libraries.** - -## Risks / notes - -- **Build feasibility**: `base-libraries` must build here (JDK + network; first - build slow). Mitigation: build only the pilot module's test+report tasks. If it - can't build, fall back to the analysis dry-run for the pilot and flag it — the - four authored files still ship. -- **Pre-existing working-tree changes**: `Jacoco.kt` (→`0.8.14`) and the old task - file are already modified. Leave `Jacoco.kt` alone (align docs to 0.8.14); only - `git mv`/rewrite the task file. -- **No commits/pushes** anywhere unless explicitly authorized. - -## Log - -- 2026-05-29 — Shortlisted candidate skills; selected `clear-solutions` as the - structural base with Kotest/taxonomy donors. -- 2026-05-29 — Recorded original decisions (mixed Java+Kotlin, Codecov + JaCoCo, - author in `config` for all repos); drafted SKILL/reference/command/install. -- 2026-05-30 — Re-scoped with the user. Renamed skill `coverage-tests` → - **`raise-coverage`**; **deferred Codecov** (JaCoCo-only v1); confirmed - Kotlin→Kotest / Java→Truth; chose `tmp/` scratch dir; set the done bar to a - local full-loop on a `base-libraries` module; added `openai.yaml` + `migrate` - prune-block deliverables. Verified the test stack and JaCoCo report paths from - `buildSrc`. Rewrote this task file from the plan; awaiting review. -- 2026-05-30 — Pivot to Kover-only. Decided to collapse the skill to a single - frontend and add a Step 0 migration gate. When the skill detects vanilla - JaCoCo, it proposes a one-shot repo-wide migration and waits for approval; - when nothing is in place, it installs Kover silently. The vanilla-JaCoCo - helpers under `buildSrc` (`jacoco-kotlin-jvm` / `jacoco-kmm-jvm` script - plugins, `JacocoConfig` aggregator and its support classes) are deprecated, - not deleted, so existing consumers keep building. Full implementation plan - moved to `.agents/tasks/raise-coverage-kover-migration.md`. -- 2026-05-30 — Built the four files + wire-up; ran the `base-libraries` pilot. - Findings that reshaped the skill: (1) consumer repos expose coverage through - **Kover** (`koverXmlReport`, JaCoCo engine, JaCoCo-format XML) — not the - per-module `jacocoTestReport` the draft assumed — so the skill is now - frontend-aware (Kover or raw JaCoCo). (2) `inline`/`reified` functions and - unreachable guards read as uncovered but are **non-actionable** (a passing test - for `parse` left `Parse.kt` `ci=0`), so the skill now filters them out. - Demonstrated true closure on `EnvironmentType.equals()`/`hashCode()` - (Java + Truth). `review-docs` running. -- 2026-05-30 — Tightened the test-generation policy in `SKILL.md` and the - Codex `default_prompt`. New tests are always written in **Kotlin** (JUnit - Jupiter + Kotest assertions), regardless of whether the code under test is - Kotlin or Java, and test class names use the **`Spec`** suffix - (`AbstractSourceFileSpec`, not `AbstractSourceFileTest`). This matches the - `*Spec.kt` convention already in use across `base-libraries` and removes - the dual-language Truth-for-Java branch that the prior pilot followed by - accident. Truth (`truth-proto-extension`) stays reachable only for - Protobuf assertions Kotest cannot express. -- 2026-05-30 — Re-ran the pilot end-to-end on `tmp/base-libraries`. Step 0 - detected vanilla JaCoCo at the root + Kover already applied in subprojects, - proposed the repo-wide migration, and on approval applied it. One lifecycle - gotcha surfaced: `KoverConfig.applyTo(root)` cannot live inside - `gradle.projectsEvaluated { … }` (Kover registers `afterEvaluate` hooks at - apply time). Documented in `references/migrate-to-kover.md` §3 and in the - `KoverConfig` class KDoc. Closed `AbstractSourceFile` with a Java + Truth - stub (6 cases via `@TempDir`); coverage went 20/2 missed LINE/BRANCH → 2/0. - Residual 2 LINE remained `mi=10 ci=0` despite passing tests — root cause is - the Spine `Exceptions.newIllegalStateException` idiom (declared to return - the exception but throws internally), making `throw helper(...)` lines - unreachable for JaCoCo's downstream probe. Added the pattern to - `references/coverage-signals.md` as a third non-actionable category. -- 2026-05-30 — Kover-only pivot landed. Implemented per - `.agents/tasks/raise-coverage-kover-migration.md`: collapsed the skill to a - single Kover frontend with a Step 0 migration gate that proposes a repo-wide - JaCoCo → Kover migration (waits for approval) when vanilla JaCoCo is - detected. Deprecated the `JacocoConfig` aggregator and its supporting helpers - (`CodebaseFilter`, `FileFilter`, `FileExtensions`, `FileExtension`, - `PathMarker`, `TaskName`) plus the `jacoco-*-jvm.gradle.kts` script plugins - — kept on disk so existing consumers keep building. Authored `KoverConfig.kt` - as the live runtime successor (preserves generated-code exclusion via Kover - `filters { excludes { classes(...) } }` derived from source dirs containing - `generated/`; wires `kover(project(...))` aggregation for every Kover-enabled - subproject; pins the JaCoCo engine version). Doc set updated: - `references/migrate-to-kover.md` is the new mechanical recipe; - `SKILL.md` got the Step 0 proposal protocol; `coverage-signals.md` rewritten - Kover-only. Reviewed across three cycles (`kotlin-review` + `review-docs` - in parallel) — all APPROVE, zero outstanding comments; - `./gradlew -p buildSrc compileKotlin` green. Nothing committed. diff --git a/.agents/tasks/archive/setup-cross-tool-agent-instructions.md b/.agents/tasks/archive/setup-cross-tool-agent-instructions.md deleted file mode 100644 index 02672e2c8..000000000 --- a/.agents/tasks/archive/setup-cross-tool-agent-instructions.md +++ /dev/null @@ -1,138 +0,0 @@ ---- -slug: setup-cross-tool-agent-instructions -branch: improve-caching -owner: claude -status: in-review -started: 2026-05-24 ---- - -# Task: Consolidate Agent Instructions into AGENTS.md - -## Goal - -Move universal agent instructions from `CLAUDE.md` into `AGENTS.md` so that -Claude Code, GitHub Copilot, and Codex all read identical rules from a single -source. Reduce `CLAUDE.md` to a thin wrapper that imports `AGENTS.md` plus a -small Claude Code-specific section. - -## Current state - -Both files already exist with real content. - -**`AGENTS.md`** currently has: -- Orientation — `project.md` reference, link to `.agents/_TOC.md` -- Commit and history safety — full rule (authoritative) -- Other safety rules — compile check, no auto-deps, no analytics, no reflection -- Moving files — `git mv` rule - -**`CLAUDE.md`** currently has: -- Project Guidelines — quick-reference-card, `project.md`, `jvm-project.md`, - skills, TOC -- Workflow Rules — `EnterPlanMode`, task planning, `api-discovery` skill, - commit rule (duplicate of AGENTS.md) -- Memory — team memory (`.agents/memory/`) + per-developer (auto-memory) -- Verification & Quality -- Core Principles -- Task Flow — plan writing, `ExitPlanMode`, `TaskCreate` -- Final Rule - -## Content split - -**Universal — move to `AGENTS.md`:** - -| Section | Notes | -|---|---| -| Project Guidelines (project.md, jvm-project.md, skills, TOC) | All agents need this orientation | -| Memory → team-shared store only (`.agents/memory/`) | Codex/Copilot have no auto-memory; the team store is universal | -| Verification & Quality | Universal engineering standards | -| Core Principles | Universal | -| Task Flow items 1, 4, 5, 6 (plan write, verify, update memory, delete task) | Universal; omit items 2–3 (ExitPlanMode/TaskCreate) | - -**Claude Code-specific — keep in `CLAUDE.md` only:** - -| Item | Why Claude-only | -|---|---| -| `EnterPlanMode` / `ExitPlanMode` | Claude Code SDK tools | -| `api-discovery` skill / never unzip JARs | Gradle cache path is machine-local | -| Per-developer auto-memory | Claude Code built-in feature | -| `TaskCreate` for live status | Claude Code SDK tool | -| Final Rule meta-note | Claude Code session advice | - -## Steps - -### 1. Expand `AGENTS.md` - -Add the universal sections to `AGENTS.md` after the existing content. Do not -duplicate the commit rule — it is already there. Resulting sections in order: - -1. Welcome / Orientation *(already exists — update to include quick-reference-card and skills references)* -2. Commit and history safety *(already exists — keep as-is)* -3. Other safety rules *(already exists — keep as-is)* -4. Moving files *(already exists — keep as-is)* -5. **Memory** — team-shared store only; omit the per-developer store -6. **Verification & Quality** -7. **Core Principles** -8. **Task planning** — write plan to `.agents/tasks/.md`; verify before marking done; delete task file on merge - -Keep `AGENTS.md` under 120 lines. Every line must change agent behaviour. - -### 2. Rewrite `CLAUDE.md` as a thin wrapper - -Replace the current content with: - -```markdown -@AGENTS.md - -## Claude Code-specific notes - -- Use Plan mode (`EnterPlanMode`) for architecture, refactoring, multi-file - changes, or lengthy documentation. Show the plan (`ExitPlanMode`) before - implementing. -- Track live progress with `TaskCreate`. -- Before reading library source code from `~/.gradle/caches`, follow the - `api-discovery` skill — never `unzip` JARs directly. -- Per-developer memory lives in the built-in auto-memory dir. Use it for - personal preferences, ephemeral project state, and per-machine resources. - Litmus test: *would a teammate benefit from this next month?* → repo. - Otherwise → auto-memory. -- This is living team memory. Update it regularly and keep it concise - (<120 lines / ~2.5k tokens). -``` - -### 3. Verify `.github/copilot-instructions.md` - -This file already exists. Confirm it contains an explicit reference to `AGENTS.md` -at the repository root, a pointer to `project.md` for repo context, and the -universal "Do not suggest" safety rules. Add the `AGENTS.md` reference if absent. - -### 4. Verify the setup - -Run these checks and report results: - -- `AGENTS.md` exists at repo root and is under 120 lines (`wc -l AGENTS.md`). -- `CLAUDE.md` first non-empty line is `@AGENTS.md`. -- `.github/copilot-instructions.md` exists and references `.agents/project.md`. -- All modified files are tracked by git (no relevant "Untracked files" in - `git status`). - -### 5. Commit - -Stage only the files modified by this task. Use this commit message: - -``` -refactor: consolidate agent instructions into AGENTS.md - -Move universal rules (orientation, memory, quality, principles, task -planning) from CLAUDE.md into AGENTS.md so Codex, Copilot, and Claude -Code all read from a single source. CLAUDE.md becomes a thin @AGENTS.md -wrapper plus Claude Code-specific notes. -``` - -## Acceptance Criteria - -- Editing `AGENTS.md` is the only required change to update agent behaviour - across all three tools. -- No universal instruction content exists only in `CLAUDE.md`. -- `AGENTS.md` is under 120 lines. -- `CLAUDE.md` first non-empty line is `@AGENTS.md`. -- All checks in step 4 pass. diff --git a/.agents/tasks/archive/testkit-coverage.md b/.agents/tasks/archive/testkit-coverage.md deleted file mode 100644 index 1ea82d255..000000000 --- a/.agents/tasks/archive/testkit-coverage.md +++ /dev/null @@ -1,64 +0,0 @@ -# Credit Gradle TestKit worker coverage to Kover - -## Problem - -Plugin tests that drive the plugin under test through Gradle TestKit's -`GradleRunner` (e.g. `LibrarySettingsPluginSpec`, `SettingsPluginSpec`) execute -that plugin in a **separate Gradle worker JVM**. Kover/JaCoCo instrument only the -test JVM, so the out-of-process execution is not credited — e.g. -`io.spine.tools.gradle.lib.LibrarySettingsPlugin.apply(Settings)` showed 0% line -coverage although it is functionally tested. - -`Plugin` classes cannot be unit-tested in-process: `org.gradle.testfixtures` -offers only `ProjectBuilder` (for `Project`), with no public `Settings` builder, so -`GradleRunner` is the only way to exercise a settings plugin. - -## Approach - -Attach the JaCoCo agent to the TestKit worker JVM and merge the resulting -execution data into the Kover report. Spans the shared test harness and build -config, so it affects every Gradle-plugin module and warrants its own review. - -1. **buildSrc — `io.spine.gradle.testing.enableTestKitCoverage()`** - (`TestKitCoverage.kt`). Resolves the standalone JaCoCo agent - (`org.jacoco:org.jacoco.agent::runtime`) and passes its path - plus a per-module exec dir (`build/jacoco-testkit`) to the `test` task as - system properties. Wipes the exec dir before each run. Applied from - `gradle-plugin-api` and `gradle-root-plugin` build scripts. - -2. **plugin-testlib — `GradleRunner.enableTestKitCoverage()`** - (`TestKitCoverage.kt`). When the system properties are present, writes a - `gradle.properties` carrying `org.gradle.jvmargs=-javaagent:…=destfile=…,append=true,…` - into the TestKit directory (the worker's Gradle user home) and points the - runner at it. One stable TestKit home per module → one worker daemon → - `append=true` accumulates all cases into `build/jacoco-testkit/testkit.exec`. - Wired into both `GradleProject.runner` and the `runGradleBuild()` helper. - No-op unless the module opted in, so other consumers are unaffected. - - The worker daemon flushes on shutdown ("after the tests complete"), before the - Kover report task runs — confirmed empirically. - -3. **buildSrc — `KoverConfig`**. Adds the per-module `testkit.exec` to the - `total` report's `additionalBinaryReports`, both per subproject (so - `::koverXmlReport` credits it) and at the root rollup. With - `useJacoco(...)`, Kover loads these `.exec` files through JaCoCo's - `ExecFileLoader`. - -## Verification (JDK 17) - -`./gradlew :gradle-plugin-api:koverXmlReport --rerun-tasks` - -| Class.method | Before | After | -|--------------------------------------|--------|-------| -| `LibrarySettingsPlugin.apply` | 0/4 | 4/4 | -| `LibrarySettingsPlugin.` | 0/2 | 2/2 | -| `SettingsPlugin.apply` (root-plugin) | 0/3 | 3/3 | - -The root `:koverXmlReport` credits the same classes (no double counting). -`ProjectExtsKt.spineExtension` stays 0/1 — it is a `reified inline` function and -is inherently uncreditable by line coverage; excluded from expectations. - -Pre-existing detekt findings in `GradleProjectSetup.kt:140` and `Sources.kt:76` -are unrelated to this change (untouched files). - -## Status: done — delete on merge to master. diff --git a/.agents/tasks/buildsrc-gradle-review-findings.md b/.agents/tasks/buildsrc-gradle-review-findings.md deleted file mode 100644 index 8890e4941..000000000 --- a/.agents/tasks/buildsrc-gradle-review-findings.md +++ /dev/null @@ -1,303 +0,0 @@ ---- -slug: buildsrc-gradle-review-findings -branch: gradle-review-skill -owner: claude -status: draft -started: 2026-05-29 -related-memories: [] ---- - -## Goal - -Apply the findings of the `/gradle-review` run against all sources -under `buildSrc/` in `config` (2026-05-29). The review found three -categories of issues: Spine mandate violations (`group = "spine"` / -`description`), Gradle correctness issues (`Provider.get()` at -configuration time, eager `Configuration`/`FileCollection` APIs that -discard task wiring), and a layer of Should-fix items around -cacheability annotations, `@PathSensitivity`, and lazy task -realisation. The work is large enough that it ships as a separate PR -from the `gradle-review-skill` branch. - -## Context - -- Review transcript: ran `/gradle-review` on the full `buildSrc/` - tree on 2026-05-29 in the `gradle-review-skill` branch. Verdict: - `REQUEST CHANGES`. -- Authoritative rules: - - [`.agents/skills/gradle-review/spine-task-conventions.md`](../skills/gradle-review/spine-task-conventions.md). - - [`.agents/skills/gradle-review/practices/tasks.md`](../skills/gradle-review/practices/tasks.md) - (ingested from the Gradle "Best practices for tasks" page). -- `SpineTaskGroup` constant already exists at - `buildSrc/src/main/kotlin/io/spine/gradle/SpineTaskGroup.kt` and is - the recommended replacement for the bare string `"spine"` (see - [`.agents/tasks/spine-task-group-constant.md`](spine-task-group-constant.md)). -- Pre-flight ripgrep confirmed: **0 hits** for `tasks.create(`, - `@CacheableTask`, `@DisableCachingByDefault`, `@UntrackedTask`, - `@PathSensitivity`, and the various `@Input*` / `@Output*` - annotations across `buildSrc/src/main/kotlin/**`. - -## Plan - -### A. Spine mandate — `group = SpineTaskGroup.name` (Must fix) - -The string `"spine"` does not appear as a task group anywhere in -`buildSrc/`. Two sub-patterns to fix: - -**A.1 — Tasks that set `group` to a non-Spine value.** - -- [x] `buildSrc/src/main/kotlin/io/spine/gradle/testing/Tasks.kt:82, 96` - — `FastTest` / `SlowTest`: replace `group = "Verification"` - with `group = SpineTaskGroup.name`. -- [x] `buildSrc/src/main/kotlin/io/spine/gradle/dart/task/DartTasks.kt:111-114` - — drop the `Group` object (`"Dart/Build"`, `"Dart/Publish"`). -- [x] `buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/JsTasks.kt:111-117` - — drop the `Group` object (`"JavaScript/Assemble"`, - `"JavaScript/Check"`, `"JavaScript/Clean"`, `"JavaScript/Build"`, - `"JavaScript/Publish"`). -- [x] Update every `Group.*` consumer to set - `group = SpineTaskGroup.name` instead: - `Webpack.kt:104`, `Check.kt:100, 129, 164, 189`, - `Assemble.kt:108, 133, 161, 188`, `Publish.kt:93, 116, 156, 187`, - `Clean.kt:90, 117`, `IntegrationTest.kt:90, 121`, - `LicenseReport.kt:80`, `dart/task/Build.kt:101, 128, 150`, - `dart/task/Publish.kt:95, 131, 163`. - -**A.2 — Tasks that set neither `group` nor `description`.** -For each, add `group = SpineTaskGroup.name` and an imperative -`description` (no trailing period): - -- [x] `buildSrc/src/main/kotlin/io/spine/gradle/javadoc/ExcludeInternalDoclet.kt:94` - (`tasks.register(taskName, Javadoc::class.java)`). -- [x] `buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt:58` - (`tasks.register(taskName, CheckVersionIncrement::class.java)`). -- [x] `buildSrc/src/main/kotlin/io/spine/gradle/github/pages/UpdateGitHubPages.kt:121, 149, 165, 183` - (`updateGitHubPages`, `copyJavadocDocs`, `copyHtmlDocs`, - `updatePagesTask`). -- [x] `buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/JacocoConfig.kt:165, 196` - (`jacocoRootReport`, `copyReports`). **Superseded by Kover-only - migration**: this file is deprecated; do not invest in micro-rewrites. - See `.agents/skills/raise-coverage/references/migrate-to-kover.md`. -- [x] `buildSrc/src/main/kotlin/io/spine/gradle/report/license/LicenseReporter.kt:111` - (`mergeAllLicenseReports`). -- [x] `buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomGenerator.kt:85` - (`generatePom`). -- [x] `buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt:235, 249, 261, 273` - (`sourcesJar`, `protoJar`, `testJar`, `javadocJar` via - `getOrCreate`). -- [x] `buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.kt:100, 121, 136` - (three registrations). -- [x] `buildSrc/src/main/kotlin/write-manifest.gradle.kts:106` - (`exposeManifestForTests`). -- [x] `buildSrc/src/main/kotlin/config-tester.gradle.kts:53` - (the script's local `clean`). -- [x] `buildSrc/src/main/kotlin/jvm-module.gradle.kts:152` - (`cleanGenerated`). - -**A.3 — Additional Spine-owned registrations uncovered during -review of Section A.** Not listed in the original `/gradle-review` -report but covered by the same mandate. All addressed in the same PR. - -- [x] `buildSrc/src/main/kotlin/DokkaExts.kt:206` - (`htmlDocsJar` via `getOrCreate`). -- [x] `buildSrc/src/main/kotlin/io/spine/gradle/dart/task/IntegrationTest.kt:76` - (`DartTasks.integrationTest`). -- [x] `buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt:157` - (`getOrCreatePublishTask` — the root-aggregator `publish` task - created when the `maven-publish` plugin is absent). -- [x] `buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt:186` - (`registerCheckCredentialsTask` — both the `register` and the - `replace` code paths). - -### B. `Provider.get()` outside a task action (Must fix) - -Each of these forces evaluation during the configuration phase, -breaking the configuration cache and serialising work Gradle would -otherwise run in parallel. - -- [x] `buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts:58-72` — - rewrite the `tasks.getting(JacocoReport::class) { ... }` block - to `tasks.named("jacocoTestReport") { ... }`, - remove the `project.layout.buildDirectory.get().asFile.absolutePath` - call, and replace the eager `walkBottomUp().toSet()` with - a lazy `DirectoryProperty`/`Provider` chain. Note - that the current code silently produces an empty set on a - clean build because `build/classes/kotlin/jvm/` does not yet - exist — that correctness bug goes away with the lazy form. - **Superseded by Kover-only migration**: this file is deprecated; - do not invest in micro-rewrites. See - `.agents/skills/raise-coverage/references/migrate-to-kover.md`. -- [ ] `buildSrc/src/main/kotlin/DokkaExts.kt:62` — change - `dokkaHtmlOutput(): File` to return `Provider` (or - a `DirectoryProperty`); update its two call sites. -- [ ] `buildSrc/src/main/kotlin/io/spine/gradle/ProjectExtensions.kt:105-106` - — `val Project.buildDirectory: File`: same pattern. Either - delete the helper (it just shells out to - `layout.buildDirectory.get().asFile`) or change it to return - `Provider`. Audit callers before deciding. -- [ ] `buildSrc/src/main/kotlin/io/spine/gradle/report/license/LicenseReporter.kt:84` - — `project.layout.buildDirectory.dir(Paths.relativePath).get().asFile`: - compose with `map` and consume inside the action. -- [x] `buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/JacocoConfig.kt:98-99` - — same pattern with the `reportsDirSuffix` directory. - **Superseded by Kover-only migration**: this file is deprecated; - do not invest in micro-rewrites. See - `.agents/skills/raise-coverage/references/migrate-to-kover.md`. -- [ ] `buildSrc/src/main/kotlin/DependencyResolution.kt:146` — - `named(configurationName).get().exclude(...)`: rewrite as - `named(configurationName) { exclude(...) }`. - -### C. Eager `FileCollection` / `Configuration` APIs (Must fix) - -These resolve configurations during configuration *and* discard -implicit task-dependency wiring — the wrong-outputs failure mode the -upstream rule warns about. - -- [ ] `buildSrc/src/main/kotlin/io/spine/gradle/javadoc/ExcludeInternalDoclet.kt:109` - — `docletpath = excludeInternalDoclet.files.toList()`: route - via a `Provider>` from `excludeInternalDoclet.elements`, - resolved inside the `Javadoc` task action. -- [x] `buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/CodebaseFilter.kt:65` - — `it.classesDirs.files.stream()`: switch to - `it.classesDirs.elements` and a lazy stream/iterator. - **Superseded by Kover-only migration**: this file is deprecated; - do not invest in micro-rewrites. See - `.agents/skills/raise-coverage/references/migrate-to-kover.md`. - -### D. Plugin task classes — caching annotations (Should fix) - -None of the three custom `DefaultTask` subclasses are annotated. -Document the contract: - -- [ ] `buildSrc/src/main/kotlin/io/spine/gradle/RunGradle.kt:48` - — add - `@DisableCachingByDefault(because = "Runs an external Gradle build whose outputs are not tracked")`. -- [ ] `buildSrc/src/main/kotlin/io/spine/gradle/publish/CheckVersionIncrement.kt:45` - — add - `@DisableCachingByDefault(because = "Performs network I/O against a Maven repository")`. -- [ ] `buildSrc/src/main/kotlin/io/spine/gradle/docs/UpdatePluginVersion.kt:56` - — add - `@DisableCachingByDefault(because = "Rewrites build scripts in place without declared outputs")`. - -### E. `@PathSensitivity` (Should fix) - -- [ ] `buildSrc/src/main/kotlin/io/spine/gradle/docs/UpdatePluginVersion.kt:58-59` - — add `@get:PathSensitive(PathSensitivity.RELATIVE)` on the - `directory` `DirectoryProperty`. The task only cares about - file names matching `build.gradle.kts` within the tree. - -### F. Eager realisation in convention code (Should fix) - -Replace eager APIs with their lazy siblings where one exists: - -- [ ] `buildSrc/src/main/kotlin/uber-jar-module.gradle.kts:73` — - `tasks.getting` → `tasks.named("publishFatJarPublicationToMavenLocal") { ... }`. -- [x] `buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts:58` — - `tasks.getting(JacocoReport::class)` → - `tasks.named("jacocoTestReport") { ... }` (folded - into B above). - **Superseded by Kover-only migration**: this file is deprecated; - do not invest in micro-rewrites. See - `.agents/skills/raise-coverage/references/migrate-to-kover.md`. -- [ ] `buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt:60` - — `tasks.getByName("check").dependsOn(this)` → - `tasks.named("check") { dependsOn(this@register) }`. -- [ ] `buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomGenerator.kt:95, 99` - — `tasks.findByName("assemble")!!` / - `tasks.findByName("build")!!` → - `tasks.named("assemble") { ... }` / `tasks.named("build") { ... }`. -- [ ] `buildSrc/src/main/kotlin/io/spine/gradle/javadoc/JavadocConfig.kt:41, 46` - — `getByName(named) as Javadoc` → `tasks.named(named)`; - ripple through callers (`ExcludeInternalDoclet.kt:93`, - `JavadocConfig.kt:73`). -- [ ] `buildSrc/src/main/kotlin/dokka-setup.gradle.kts:51` — replace - `val kspKotlin = tasks.findByName("kspKotlin")` inside - `afterEvaluate { ... }` with a - `tasks.matching { it.name == "kspKotlin" }.configureEach { ... }` - block, or rely on `tasks.named("kspKotlin").orNull`. - -### G. `dependsOn` where input/output wiring expresses the link (Should fix) - -- [ ] `buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt:273-278` - — `javadocJar()`: replace - `from(layout.buildDirectory.dir("dokka/javadoc"))` + - `dependsOn("dokkaGeneratePublicationJavadoc")` with - `from(tasks.named("dokkaGeneratePublicationJavadoc").map { it.outputs.files })`. -- [ ] `buildSrc/src/main/kotlin/DokkaExts.kt:206-213` — - `htmlDocsJar()`: same pattern; wire - `from(tasks.dokkaHtmlTask().map { it.outputs.files })` and - remove the explicit `dependsOn(dokkaTask)`. -- [x] `buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/JacocoConfig.kt:196-203` - — drop the trailing `dependsOn(projects.map { ... })` once - `everyExecData` is verified to be a Provider-typed chain that - carries producer dependencies. - **Superseded by Kover-only migration**: this file is deprecated; - do not invest in micro-rewrites. See - `.agents/skills/raise-coverage/references/migrate-to-kover.md`. -- [ ] `buildSrc/src/main/kotlin/io/spine/gradle/report/license/LicenseReporter.kt:117-118, 123` - — the explicit `consolidationTask.dependsOn(perProjectTask)` - and `perProjectTask.dependsOn(assembleTask)` should be - expressed via the merge task's `@InputFiles` on the per-project - report files. Refactor `mergeReports` to take a - `ListProperty` and let Gradle infer ordering. - -### H. Nits - -- [ ] **Trailing period in `description`** — strip from every Dart/JS - task helper (`Webpack.kt:103`, `Check.kt:99, 128, 163, 188`, - `Assemble.kt:107, 132, 160, 187`, `Publish.kt:92, 115, 155, 186`, - `Clean.kt:89, 116`, `IntegrationTest.kt:88, 120`, - `LicenseReport.kt:79`, `dart/task/Build.kt:100, 127, 149`, - `dart/task/Publish.kt:94, 130, 162`) and from - `testing/Tasks.kt:81, 95`. -- [ ] **KDoc back-link.** Add a KDoc link to the relevant rule from - [`.agents/skills/gradle-review/practices/tasks.md`](../skills/gradle-review/practices/tasks.md) - (or the upstream Gradle page) on each of `RunGradle.kt`, - `CheckVersionIncrement.kt`, `UpdatePluginVersion.kt`. -- [ ] **`project` access inside task actions** — `RunGradle.kt:142-180` - (`project.rootDir`, `project.gradle.taskGraph.hasTask(":clean")`, - `project.file(directory)`, `project.rootProject`), - `CheckVersionIncrement.kt:60-115` (`project.artifactPath()` and - friends), `PomGenerator.kt:85-93`, - `LicenseReporter.kt:120-122`. Capture the necessary values or - `Provider`s during configuration; pass them in via task - properties. -- [ ] **`@Internal lateinit var directory: String` in `RunGradle.kt:60-62`** - — should be a `DirectoryProperty` (or at least a - `Property`) so the task can participate in - configuration-cache serialisation. - -### Verification - -- [ ] Run `./gradlew clean build` against `config` and confirm it - passes without configuration-cache warnings. -- [ ] Re-run `/gradle-review` against `buildSrc/` and confirm - `APPROVE` (or `APPROVE WITH CHANGES` for residual Nits). -- [ ] Smoke-test downstream consumers (`base`, `base-types`, - `core-java`) via the `buildDependants` task in - `config-tester.gradle.kts`. - -## Decisions - -- **Scope.** All three findings categories (Must fix, Should fix, - Nits) are in scope. The volume justifies a dedicated PR. -- **Sequencing.** Sections A and B-C are independent and can be done - in either order. Section G depends on the lazy-Provider rewrites - in B for `DokkaExts.kt`. Run the verification step after every - section to keep the PR bisectable. -- **Out of scope.** `io.spine.dependency.*` files (owned by the - `dependency-audit` skill) and `gradlew` / `gradlew.bat` are - excluded from this task. - -## Log - -- 2026-05-29 — drafted from the `/gradle-review` run on the full - `buildSrc/` tree. Branch `gradle-review-skill` carries the - document; execution lands in a separate PR. -- 2026-05-29 — Section A applied on branch - `address-gradle-review-01`. Five review rounds against the diff - surfaced four additional Spine-owned task registrations that the - original report missed (`htmlDocsJar`, dart `integrationTest`, the - root-aggregator `publish` task, and `checkCredentials`); all four - added to Section A.3 and addressed in the same PR. Sections B–H - remain pending. diff --git a/.agents/tasks/cross-agent-skill-best-practices.md b/.agents/tasks/cross-agent-skill-best-practices.md deleted file mode 100644 index 4876f28c8..000000000 --- a/.agents/tasks/cross-agent-skill-best-practices.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -slug: cross-agent-skill-best-practices -branch: codex/audit-skills-discoverability -owner: codex -status: draft -started: 2026-05-31 ---- - -## Goal - -Bring the repository skills in `.agents/skills/` closer to the shared skills -standard so they are easy to discover and execute across Codex, Claude, and -other compatible agents. Success means a new agent can identify the right skill -from metadata, load a short `SKILL.md`, follow agent-neutral instructions, and -delegate deterministic work to scripts or references where appropriate. - -## Context - -- Audit source: Claude skill authoring best practices.[^claude-best-practices] -- Current inventory: 16 skills, 16 `SKILL.md` files, and 16 - `agents/openai.yaml` files. -- Good baseline: skill directory names match frontmatter names, names use the - expected lowercase hyphenated form, all `SKILL.md` files are under the - 500-line guideline, and frontmatter descriptions are under 1024 characters. -- User direction: optimize for compatibility with Codex, Claude, and other AI - agents that support the skills standard, not for a single agent runtime. - -## Findings - -1. Some fragile deterministic workflows are still mostly prose instead of - scripts. - - `check-links` embeds site detection, binary preflight, Lychee download, - Hugo server lifecycle, reporting, and sentinel writing in `SKILL.md`. - - `dependency-update` asks the agent to parse Kotlin dependency files, - discover versions, compare versions, and edit files manually. - - Best-practice risk: high-cognitive-load procedures are harder for agents - to pick up reliably and should be moved behind deterministic entrypoints - where practical. - -2. `raise-coverage` has a high-impact automatic path. - - The skill silently installs Kover when no coverage plugin is present. - - Best-practice risk: a request to add tests can mutate build configuration - without an explicit approval checkpoint. - - Cross-agent concern: different agents may interpret "silent install" - differently, so this should become an explicit policy decision. - -3. Long reference files need top-level contents. - - `raise-coverage/references/coverage-signals.md` is 181 lines. - - `raise-coverage/references/migrate-to-kover.md` is 352 lines. - - `gradle-review/practices/tasks.md` is 147 lines. - - Best-practice risk: reference material over 100 lines should be easier to - skim before an agent loads or follows a specific section. - -4. Some metadata and prompt surfaces are less portable than the rest. - - `raise-coverage/agents/openai.yaml` has a much longer `default_prompt` - than other skills. - - `writer/agents/openai.yaml` does not mention `$writer`, unlike the other - skill prompts. - - `raise-coverage/SKILL.md` still uses slash-command phrasing such as - `/raise-coverage` and `/version-bumped`, which is less portable across - agents. - -5. Evaluation evidence is missing. - - No eval or scenario files were found under `.agents/skills/`. - - Only `update-copyright` currently has script tests. - - Best-practice risk: the repo does not make it visible that skills were - tested on realistic examples, so future agents cannot distinguish - validated workflows from untried instructions. - -## Plan - -- [ ] Decide whether `raise-coverage` may silently install Kover, or whether all - build-configuration edits require explicit approval. -- [ ] Extract or introduce deterministic entrypoints for the highest-risk - procedural skills, starting with `check-links` and `dependency-update`. -- [ ] Add table-of-contents sections to reference files over 100 lines. -- [ ] Normalize cross-agent phrasing by removing slash-command assumptions and - keeping instructions skill-name based. -- [ ] Shorten unusually long `openai.yaml` default prompts while preserving - discoverability for Codex. -- [ ] Decide whether to add lightweight skill scenarios or eval notes for the - major skills. -- [ ] Re-audit all skills against the Claude best-practices checklist and record - the result in this task log. - -## Open Decisions - -- Should `raise-coverage` require approval before any Kover installation, even - when no coverage plugin exists? -- Should `dependency-update` get a real implementation script now, or should the - first pass only split parsing/versioning rules into references? -- What is the desired minimum evaluation artifact: short scenario files, - executable tests, or both? - -## Log - -- 2026-05-31: Drafted from the cross-agent skills best-practices audit. Awaiting - maintainer review before changes. - -[^claude-best-practices]: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices diff --git a/.agents/tasks/enforce-max-line-length.md b/.agents/tasks/enforce-max-line-length.md deleted file mode 100644 index 3cad1c696..000000000 --- a/.agents/tasks/enforce-max-line-length.md +++ /dev/null @@ -1,279 +0,0 @@ ---- -slug: enforce-max-line-length -branch: address-gradle-review-01 -owner: claude -status: draft -started: 2026-05-29 -related-memories: [] ---- - -## Goal - -Extend the agent-facing instructions and skills under `.agents/` so -that detekt's `MaxLineLength` rule -(`buildSrc/quality/detekt-config.yml:19-21`, -`maxLineLength: 100`, `excludeCommentStatements: true`) is honoured at -author time and surfaced at review time, instead of being discovered -late by CI on GitHub. - -Severity by file type: - -- **Detekt-enforced → Must fix** — non-comment lines in `.kt` / `.kts` - over the configured limit. These break `./gradlew build`. -- **Repo policy → Should fix** — KDoc / Javadoc body lines in any - source extension; `.java` lines; `.proto` lines; `.md` lines - (incl. `README.md`, `docs/**`, `.agents/**`). Detekt does not flag - these; the reviewer skills do. - -## Context - -CI and local builds repeatedly fail on detekt's `MaxLineLength` rule. -The user finds the late discovery — especially on GitHub — annoying. -None of the current agent instructions or skills name the rule, so -agents write code that breaks the build, then have to retry. - -### Framing - -The numeric threshold is a configuration parameter, not a constant. - -**Author-time behaviour**: agents read `MaxLineLength.maxLineLength` -from `buildSrc/quality/detekt-config.yml` once per session and treat -the value as a session-local constant. This is workable; re-reading -the YAML for every line of output is not. - -**Guidance text**: the new sections never bake the literal number -into `.agents/` prose. They reference the rule name and the file -path. If the threshold changes, the agent's session-start lookup -picks up the new value with no doc edit. - -**Review-time behaviour**: when a reviewer surfaces a finding, the -report cites the actual value (`"line 47 is 108 chars (limit 100, -from buildSrc/quality/detekt-config.yml)"`). The number lands in the -report, not in the rule. - -### KDoc handling (empirically verified) - -`excludeCommentStatements: true` excludes lines whose statement is a -comment — single-line `//`, trailing `//`, and KDoc body lines. The -exclusion of KDoc bodies is confirmed by -`buildSrc/src/main/kotlin/detekt-code-analysis.gradle.kts:52`, a -115-character KDoc body line that ships in the codebase and passes -the detekt build today. KDoc body lines are therefore Should-fix -repo policy, not Must-fix. - -### Splitting / restructure rules (confirmed with user) - -- String literals (including URLs inside strings) split at a - meaningful boundary into ≥ 2 `+`-concatenated pieces — never - truncated. -- Long imports: prefer an import alias - (`import a.b.c.LongName as Short`). If unavailable, a - `@file:Suppress("MaxLineLength")` is acceptable. -- Other unbreakable tokens (`[name][some.long.FQN]` in KDoc; long - generated identifier): prefer restructure (intermediate `val`, - reference-style Markdown link, alias). When no restructure is - reasonable, use `@Suppress("MaxLineLength")` on the declaration - with a brief `// Reason: …` comment. Use `@file:Suppress` only for - file-scope cases (e.g., a long import that cannot be aliased). - -### Scope clarifications - -- **Generated sources excluded**: do not flag lines under - `**/generated/**` or `**/generated-proto/**` — these are the paths - Spine's `buildSrc/quality/checkstyle.xml:35-42` and - `buildSrc/quality/pmd.xml:36-37` already exclude from the other - static-analysis runs. -- **Reading context vs. reporting scope.** Reviewers continue to read - each affected file fully (existing `kotlin-review` rule at - `.agents/skills/kotlin-review/SKILL.md:31-32`). They only *report* - line-length findings on lines the diff touched - (`git diff -U0 ...HEAD`). Pre-existing long lines are not - flagged. The two rules co-exist: read all, report changed. -- **`module.gradle.kts` carve-out**: per `AGENTS.md § Code review`, - in a consumer repo `buildSrc/src/main/kotlin/module.gradle.kts` is - in scope for the reviewers; it follows the same Must-fix rule as - any other `.kts`. -- **YAML lookup is from `HEAD`, not the base ref.** Long-lived - branches sometimes change `detekt-config.yml` mid-branch; reviewers - always re-read the value from the working tree, so the rule matches - what `./gradlew build` will see. -- **YAML missing is a hard error.** If - `buildSrc/quality/detekt-config.yml` is absent or lacks - `MaxLineLength.maxLineLength`, the reviewer reports a Must-fix - asking the user to restore the config rather than silently - inventing a number. - -## Plan - -Six `.agents/` Markdown files. No code or build changes. New lines -wrap at the configured limit. - -### 1. `.agents/coding-guidelines.md` - -- [ ] Add a new top-level `## Line length` section, placed immediately - after the existing "Text formatting" section. The canonical - content lives here; other docs cross-reference this heading. - Cover: - - Source-of-truth lookup: read `MaxLineLength.maxLineLength` from - `buildSrc/quality/detekt-config.yml` once at session start. Never - write the literal number into the guideline. - - Severity split (detekt-enforced vs. repo policy) per Context above. - - String-literal strategy with a small example whose split is at a - URL path boundary, e.g. - - ```kotlin - val ref = "https://github.com/SpineEventEngine/config/blob/master/" + - "buildSrc/quality/detekt-config.yml" - ``` - - This covers the URL-splitting case the user called out; the - existing `JacocoConfig.kt:122-125` pattern splits prose, not a - URL, and is not a sufficient teacher on its own. - - Unbreakable-token rules: import alias, restructure, then - `@Suppress` placement (on the declaration; `@file:Suppress` for - file-scope). - - Scope exclusions: generated sources; changed lines only. - -### 2. `.agents/documentation-guidelines.md` - -- [ ] Append one bullet to "Commenting guidelines": - - > Wrap KDoc / Javadoc body lines and Markdown body lines at the - > limit defined in `buildSrc/quality/detekt-config.yml` - > (`MaxLineLength.maxLineLength`). See - > `coding-guidelines.md § Line length` for the splitting strategy. - - Single sentence; no duplication of the canonical section. - -### 3. `.agents/quick-reference-card.md` - -- [ ] Rewrap the existing 135-char line 3 so the card itself respects - the rule it now advertises. -- [ ] Append one line (plain text, no decorative emoji — the rest of - the card uses 🚫 for a hard prohibition only, and line-length - guidance isn't in that category): - - > At session start, read `MaxLineLength.maxLineLength` from - > `buildSrc/quality/detekt-config.yml` and wrap new lines under it. - > See `coding-guidelines.md § Line length`. - -### 4. `.agents/skills/kotlin-review/SKILL.md` - -- [ ] In "Review procedure" step 3 (the coding-guidelines checklist), - append: - - > Line length (`MaxLineLength`). The reviewer reads the limit from - > `buildSrc/quality/detekt-config.yml` and applies it only to lines - > the diff touched. Non-comment `.kt` / `.kts` lines over the limit - > are **Must fix** (detekt breaks the build; - > `excludeCommentStatements: true` exempts KDoc bodies from the - > build break). KDoc bodies in `.kt` / `.kts`, and any `.java` line - > over the limit, are **Should fix**. For changed lines inside a - > string literal the fix is splitting into ≥ 2 `+`-concatenated - > pieces; otherwise follow `coding-guidelines.md § Line length`. - -- [ ] Update "Output format" correspondingly: add the bucket entries - but keep the existing Must / Should / Nits semantics unchanged. - -### 5. `.agents/skills/review-docs/SKILL.md` - -- [ ] Insert into "Checks → A. KDoc / Javadoc inside sources": - - > **Line length.** KDoc / Javadoc body lines wrap at the limit from - > `buildSrc/quality/detekt-config.yml`. Long body lines are - > **Should fix**; code lines around the comment, if also too long, - > are owned by `kotlin-review`. - -- [ ] Insert into "Checks → B. Markdown docs": - - > **Line length.** Body lines in `.md` — including `README.md`, - > `docs/**`, and `.agents/**` (this expands the skill's prior `.md` - > scope explicitly) — wrap at the configured limit. Long URLs go in - > reference-style footnote definitions. Long lines are - > **Should fix**. - -### 6. `.agents/skills/pre-pr/SKILL.md` - -- [ ] In the "Procedure" section, add a one-line pointer near the - existing reviewer-dispatch table (around - `.agents/skills/pre-pr/SKILL.md:104-106`): - - > Line-length findings on changed Kotlin / Java / Markdown lines - > are reported by the dispatched reviewers (`kotlin-review`, - > `review-docs`). pre-pr itself does not re-check. - - Documentation only — no logic change. Clarifies that the rule is - inherited via the existing dispatch and prevents future edits from - duplicating the check inside pre-pr. - -### Verification - -- [ ] Visually scan every edited file for the literal `100`. The - number should not appear in the new prose; only the rule name - and the YAML path should. -- [ ] Read the YAML, capture the value - (`LIMIT=$(awk '/maxLineLength:/ {print $2}' - buildSrc/quality/detekt-config.yml)`), and run - `awk -v n=$LIMIT 'length > n' `. `awk`'s - `length` counts bytes; for the ASCII prose introduced here that - matches characters, but a non-ASCII glyph in future edits would - miscount. Acceptable for this change. -- [ ] Sanity-check cross-references: every `coding-guidelines.md § - Line length` link resolves to the new top-level section heading. -- [ ] Spot test the author behaviour. In a fresh session, ask the - agent to write a long Kotlin string literal containing a URL; - confirm the result splits with `+` at a URL path boundary and - preserves every character. -- [ ] Spot test the reviewer behaviour. Synthesize a diff with: one - non-comment `.kt` line over the limit (expect Must fix); one - KDoc body line over the limit (expect Should fix); one `.java` - line over the limit (expect Should fix); one `.md` body line - over the limit (expect Should fix). Run `kotlin-review` and - `review-docs` and confirm bucketing. -- [ ] Confirm the missing-YAML behaviour: temporarily move - `buildSrc/quality/detekt-config.yml` aside, run a reviewer over - a synthetic diff, confirm it reports a **Must fix** asking the - user to restore the config (not a silent fallback). - -## Out of scope - -- `buildSrc/quality/detekt-config.yml` — unchanged. -- `writer/SKILL.md` and `java-to-kotlin/SKILL.md` — they author, they - don't enforce. The canonical rule in `coding-guidelines.md` reaches - them by reference. -- `gradle-review/SKILL.md` — `.kts` files are reviewed by - `kotlin-review` (via pre-pr's `code` dispatch). Adding a second - owner would double-report; defer to `kotlin-review § Line length`. -- `update-copyright/SKILL.md` — if a header rewrite produces a long - line, the reviewer will catch it; no skill-local rule. -- `memory/MEMORY.md` and `_TOC.md` — the rule is durable team policy - belonging in `.agents/`, indexed via the natural section heading. -- Rewrap of pre-existing over-length lines outside the diff (e.g., - `java-to-kotlin/SKILL.md:24,25,40,42`) — separate cleanup task, not - blocked by this plan. - -## Decisions - -- **KDoc severity**. Should-fix, not Must-fix. Empirically verified - by `buildSrc/src/main/kotlin/detekt-code-analysis.gradle.kts:52` - (115-char KDoc body line that ships and builds clean). -- **`gradle-review` not edited**. `.kts` files flow through - `kotlin-review` already (via pre-pr's `code` dispatch); a second - owner in `gradle-review` would cause double-reports for the same - finding. The trade-off is that manual `/gradle-review` runs without - a paired `/kotlin-review` will not surface line-length findings on - `.kts` files; users running only `gradle-review` are looking for - Gradle conventions, not detekt rules, so the gap is acceptable. -- **YAML lookup at session start, not per line**. Re-reading the YAML - for every line of output is impractical; the agent caches the value - as a session-local constant. Documentation never bakes the literal. -- **Missing YAML is Must-fix, not informational**. Avoids silent - fallback drift. - -## Log - -- 2026-05-29 — drafted in this session; plan revised twice to address - findings from two review rounds (KDoc empirics, generated-source - globs, `## Line length` heading placement, `gradle-review` → - `pre-pr` swap, YAML-missing severity, verification cleanup). - Awaiting approval. diff --git a/.agents/tasks/gradle-caching-plan.md b/.agents/tasks/gradle-caching-plan.md deleted file mode 100644 index efc3859ff..000000000 --- a/.agents/tasks/gradle-caching-plan.md +++ /dev/null @@ -1,200 +0,0 @@ ---- -slug: gradle-caching-plan -branch: gradle-review-skill -owner: claude -status: draft -started: 2026-05-29 ---- - -# Plan: Speed Up Builds via Gradle Caching (org-wide, through `config`) - -> Implementation plan for Claude Code operating in the **`SpineEventEngine/config`** repository. -> Follow the repo's existing conventions in `CLAUDE.md` / `.agents/` (commit style, copyright -> headers, Kotlin guidelines, allowed commands). Make minimal diffs and land each phase as its -> own PR. - -## Purpose - -Make CI and local builds across the Spine organization faster by enabling **every free Gradle -caching layer**. Because `config` is the shared submodule pulled into every Spine repository, -changes here propagate org-wide — no per-repo edits required. - -## Why this work belongs in `config` - -`config` is added to each Spine project as a Git submodule, and `./config/pull` copies shared -files into the consuming project. Two of those files are exactly the levers we need: - -- **Root `gradle.properties`** — *overwritten* into each consuming repo on every `pull`. This is - the single source of truth for Gradle build flags. -- **`.github-workflows/`** — its workflow scripts are *merged into* each repo's - `.github/workflows/` on `pull`. This is where the CI definitions that run in every repo live. - (Per the repo README, these workflows intentionally do **not** run for `config` itself, so they - live under `.github-workflows/` rather than `.github/workflows/`.) - -Editing these here, then bumping the submodule + running `./config/pull` in a consuming repo, is -how the change reaches the whole org. - -## Goal - -Enable, in order of safety/ROI: - -1. **Dependency cache** — downloaded dependencies + wrapper distributions. -2. **Local build cache** — task outputs (`caches/build-cache-1`), persisted across CI runs so cold - CI builds skip unchanged work. -3. **Configuration cache** — skip Gradle's configuration phase on repeat runs (gated; higher risk). - -**Non-goal (out of scope here):** a *remote* build cache (Develocity or a self-hosted cache node). -That is the only layer that shares task outputs *across* repositories and across machines, but it -requires infrastructure (a reachable cache node + credentials, or Develocity) and is not a -config-only change. It is captured as a future phase, not to be implemented now. - -## Mental model (so changes are made for the right reasons) - -- The **dependency cache** speeds up *resolution/download*; it does not reuse build work. -- The **build cache** reuses *task outputs*, keyed by a hash of their inputs. Gradle's up-to-date - checks already cover "same workspace, nothing changed," so the build cache only adds value from a - **cold/fresh state** with unchanged inputs. -- **CI is cold on every run** (fresh checkout), so the build cache is precisely what helps CI — - independent of team size or number of repos. -- `gradle/actions/setup-gradle` persists the Gradle User Home (deps, wrapper, **and** - `caches/build-cache-1`) via the GitHub Actions cache. By default it **writes** the cache only - from the **default branch**; other branches **read** the default branch's cache. So PR builds - reuse what `main`'s CI produced, without polluting the shared cache. - - Caveat: for `pull_request`-triggered runs, the cache scope is the PR merge ref and writes are - disabled by default (only re-runs of the same PR restore them). The read-from-`main` behavior - still applies. - -## Guardrails (do / don't) - -- ✅ **DO** edit the **root `gradle.properties`** in `config` for all Gradle flags. -- ⛔ **DON'T** add Gradle flags to individual consuming repos' `gradle.properties` — `./config/pull` - overwrites that file, so such edits are lost. `config` is the only correct place. -- ✅ **DO** edit workflow templates in **`.github-workflows/`** (and, if you also want `config`'s - own CI to benefit, `config`'s own `.github/workflows/`). -- ⛔ **DON'T** keep `actions/setup-java` with `cache: gradle` alongside `setup-gradle` — the two - caching mechanisms conflict; remove `cache: gradle` when adding `setup-gradle`. -- ⛔ **DON'T** create any remote cache server, add secrets, create accounts, or change repo - permissions. (Out of scope; infra/owner decisions.) -- ✅ Keep diffs minimal: don't reorder or delete existing properties/steps that are unrelated. -- ✅ Land each phase as a **separate commit/PR** and validate before moving on. - -## Tasks - -### Phase 0 — Inventory (no changes) - -1. Read the root `gradle.properties`; record which `org.gradle.*` flags already exist (caching, - parallel, configuration-cache, jvmargs, etc.). -2. List `.github-workflows/`. For each workflow, locate the Java/Gradle setup steps and how Gradle - is invoked (`./gradlew ...`). Note any use of `actions/setup-java` with `cache: gradle`. -3. Check `config`'s own `.github/workflows/` separately (these run for `config` itself). -4. Read `gradle/wrapper/gradle-wrapper.properties` to determine the **Gradle version**. The stable - configuration-cache property names below assume Gradle **8.1+**; if older, adjust property names - and treat Phase 3 with extra caution. -5. Summarize findings before editing. - -### Phase 1 — Switch CI to `gradle/actions/setup-gradle` - -For each relevant workflow: - -- Remove `cache: gradle` from any `actions/setup-java` step. -- Add a `gradle/actions/setup-gradle@v6` step **after** Java setup and **before** any Gradle - invocation. (The action also configures init-scripts that apply to later `run: ./gradlew` steps.) -- Match the repo's existing action-pinning policy; current major versions available are - `actions/checkout@v6`, `actions/setup-java@v5`, `gradle/actions/setup-gradle@v6`. - -Reference shape (adapt to each workflow's actual jobs/matrix — do not blindly overwrite): - -```yaml -steps: - - uses: actions/checkout@v6 - - uses: actions/setup-java@v5 - with: - distribution: temurin - java-version: 17 # keep whatever the repo currently targets; no `cache: gradle` - - uses: gradle/actions/setup-gradle@v6 - - run: ./gradlew build -``` - -Notes: -- The default `enhanced` cache provider is **free for public repositories** (all Spine repos are - public). No `cache-provider` override needed unless a fully-MIT path is preferred - (`cache-provider: basic`). -- Leave the default write-on-default-branch-only behavior in place; it's the desired setup. - -### Phase 2 — Enable build cache + parallel in shared `gradle.properties` - -In the root `gradle.properties`, add (only if absent): - -```properties -org.gradle.caching=true -org.gradle.parallel=true -``` - -- `caching=true` enables the **local** build cache; combined with `setup-gradle` persisting - `caches/build-cache-1`, CI runs now reuse task outputs. -- `parallel=true` is generally safe but must be validated (see acceptance). - -### Phase 3 — Configuration cache (gated; higher risk) - -In the root `gradle.properties`, add: - -```properties -org.gradle.configuration-cache=true -org.gradle.configuration-cache.problems=warn -``` - -- Start in **warn** mode so configuration-cache-incompatible tasks **do not fail** the build. -- Spine relies on many custom Gradle plugins and code-generation tasks (Protobuf / model compiler - / etc.) that may not yet be configuration-cache compatible. Warn mode surfaces problems without - breaking builds. -- Where feasible, fix incompatibilities in **`buildSrc`** (the shared build logic). If problems are - extensive, **leave configuration cache in warn mode or defer Phase 3 entirely** — do **not** - switch to strict/fail mode until `buildDependants` is clean. -- (On Gradle < 8.1 the stable property differs; do not guess — check the wrapper version from - Phase 0 and use the matching property name, or skip this phase.) - -### Phase 4 — Remote build cache (FUTURE — do not implement now) - -Documented for completeness only. If pursued later: -- Configure `buildCache { remote(HttpBuildCache) { ... } }` (in `settings.gradle.kts` of consuming - projects, or centrally via `buildSrc`), pushing **only from CI**. -- Per Gradle's guidance, **disable the local build cache on CI** when a remote cache is available, - to keep GitHub Actions cache entries small. -- Requires a reachable cache node + credentials (or Develocity) and is an infrastructure decision — - not a config-only change. Stop and flag this to a human rather than implementing it. - -## Verification / acceptance criteria - -`config` ships `ConfigTester`, wired into `build.gradle.kts` as the `buildDependants` task, which -checks out and builds the dependant repos (`base`, `base-types`, `core-java`) against the **local** -`config`. Use it as the gate for every phase: - -```bash -./gradlew clean buildDependants # ~30+ minutes; builds base, base-types, core-java with local config -``` - -Acceptance for each phase: - -1. `buildDependants` **passes** with the change applied. -2. **Cache reuse is observable:** run a dependant build twice; the second run shows many tasks as - `FROM-CACHE` / `UP-TO-DATE`. -3. **CI evidence:** in a workflow run, the `setup-gradle` **Job Summary** reports cache entries - restored/saved; compare overall job wall-clock **before vs after**. -4. **Phase 3 specifically:** `buildDependants` completes with configuration cache enabled (warn mode - acceptable). Record any remaining configuration-cache problems in the PR description. - -## Rollout - -1. Land Phases 1–2 (and 3 if clean) as separate PRs in `config`. -2. Pilot in **one** consuming repo first (suggest `base`): bump the `config` submodule, run - `./config/pull` (this overwrites `gradle.properties` and merges `.github-workflows/` into - `.github/workflows/`), confirm CI is green and faster. -3. Propagate to the remaining repos once the pilot is validated. - -## References - -- `setup-gradle` docs: https://github.com/gradle/actions/blob/main/docs/setup-gradle.md -- Gradle Build Cache: https://docs.gradle.org/current/userguide/build_cache.html -- Gradle Configuration Cache: https://docs.gradle.org/current/userguide/configuration_cache.html -- `config` README (pull mechanism, `.github-workflows`, `ConfigTester`/`buildDependants`): - https://github.com/SpineEventEngine/config diff --git a/.agents/tasks/spine-task-group-constant.md b/.agents/tasks/spine-task-group-constant.md deleted file mode 100644 index 24667819f..000000000 --- a/.agents/tasks/spine-task-group-constant.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -slug: spine-task-group-constant -branch: gradle-review-skill -owner: claude -status: in-progress -started: 2026-05-29 -related-memories: [] ---- - -## Goal - -Replace the bare string literal `"spine"` (the Gradle task group used -by every custom task in this organisation) with a shared constant in -two locations: - -1. **In `config`'s `buildSrc/`** — so all build files in `config` and - all consumer projects that apply `config` reference the same - symbol instead of repeating the literal. -2. **In `tool-base`** — so the production code of every Spine SDK - Gradle plugin references the same symbol when it registers or - configures tasks. - -Once both constants exist, `gradle-review` reports a remaining bare -literal `"spine"` as a Nit and recommends the relevant constant as -the replacement. - -## Context - -- The Spine convention "every custom task has `group = "spine"`" is - documented in - [`.agents/skills/gradle-review/spine-task-conventions.md`](../skills/gradle-review/spine-task-conventions.md). -- The `gradle-review` skill (see - [`../skills/gradle-review/SKILL.md`](../skills/gradle-review/SKILL.md)) - enforces the rule, and lists the constant migration as a Nit until - the symbol exists. -- Two separate codebases are involved because of dependency direction: - `buildSrc/` in `config` is on the build classpath of every consumer - project's `build.gradle.kts`, while `tool-base` is consumed at - runtime by SDK plugins. A single source-of-truth in `tool-base` and - a re-export from `buildSrc/` would couple the two — instead each - side declares its own constant and both keep the same value - (`"spine"`). The `gradle-review` skill cross-checks both. - -## Plan - -### A. `config/buildSrc` constant - -- [x] Add `object SpineTaskGroup { const val name = "spine" }` in - `buildSrc/src/main/kotlin/io/spine/gradle/SpineTaskGroup.kt` - with copyright header and KDoc referencing - `.agents/skills/gradle-review/spine-task-conventions.md`. -- [x] Migrate every `group = "spine"` usage in `buildSrc/**/*.kt` and - `buildSrc/**/*.gradle.kts` to the constant. (Verified by - `rg "group\s*=\s*\"spine\""` — no existing literals in - `buildSrc/`; the only `"spine"` occurrence there is the - unrelated artifact-prefix constant in `dependency/local/Base.kt`.) -- [x] Migrate every `group = "spine"` usage in the project's - `build.gradle.kts` and `settings.gradle.kts` (the constant is - visible from build files thanks to `buildSrc/`). (Verified — no - existing literals.) -- [x] Spot-check with `rg -n '"spine"' --type kotlin` (ripgrep's - built-in `kotlin` type covers both `*.kt` and `*.kts`; the - short alias `--type kt` is **not** recognised) — only the - constant declaration and unrelated occurrences (artifact - prefix in `Base.kt`, exclude rule for `spine-base` in - `DependencyResolution.kt`) remain. - -### B. `tool-base` constant + GitHub issue - -- [x] Open the tracking issue under `tool-base` — [tool-base#171][tool-base-171]. -- [ ] (Remaining migration is tracked by that issue, not this branch.) - -[tool-base-171]: https://github.com/SpineEventEngine/tool-base/issues/171 - -## Decisions - -- **Naming and shape.** `object SpineTaskGroup { const val name = "spine" }`. - Reference site reads `group = SpineTaskGroup.name`. Mirrors the - `JsTasks.Group.build` precedent already used inside `buildSrc/` and - leaves room for related constants later. Consistency with the - `tool-base` constant — once it exists — is more important than the - specific shape; the `tool-base` issue should adopt the same shape. -- **Location.** New file at - `buildSrc/src/main/kotlin/io/spine/gradle/SpineTaskGroup.kt`, - alongside `TaskName.kt` and other top-level Gradle helpers. - Visibility is `public` (default) so consumer `build.gradle.kts` - files can import the symbol. -- **KDoc link form.** Plain text path to - `.agents/skills/gradle-review/spine-task-conventions.md`; KDoc does - not resolve relative Markdown links in the IDE, and an absolute - GitHub URL would couple the source to a specific branch. - -## Log - -- 2026-05-29 — drafted alongside the `gradle-review` skill, awaiting - approval to start migration. -- 2026-05-29 — implemented `SpineTaskGroup` in `config/buildSrc` - (`io.spine.gradle.SpineTaskGroup`). Verified by ripgrep that no - bare `"spine"` task-group literals exist in `*.kt` or `*.gradle.kts` - under this repo, so the migration step in section A is a no-op - inside `config`. The constant is in place for new tasks added here - and for consumer repositories' build files. The `tool-base` - constant and its migration remain tracked under - [tool-base#171][tool-base-171]. diff --git a/.github/workflows/ensure-reports-updated.yml b/.github/workflows/ensure-reports-updated.yml index 0cf35a6d9..20deb3f69 100644 --- a/.github/workflows/ensure-reports-updated.yml +++ b/.github/workflows/ensure-reports-updated.yml @@ -1,16 +1,26 @@ # 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@v6 diff --git a/.github/workflows/increment-guard.yml b/.github/workflows/increment-guard.yml index 6399c87af..f20b4bed6 100644 --- a/.github/workflows/increment-guard.yml +++ b/.github/workflows/increment-guard.yml @@ -1,17 +1,27 @@ -# Ensures that the current lib version is not yet published but executing the Gradle +# Ensures that the current lib version is not yet published by executing the Gradle # `checkVersionIncrement` task. +# +# 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: jobs: check: name: Check version increment 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@v6 diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt index 14304638b..7b1160d3b 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt @@ -72,7 +72,7 @@ object Compiler : Dependency() { * The version of the Compiler dependencies. */ override val version: String - private const val fallbackVersion = "2.0.0-SNAPSHOT.046" + private const val fallbackVersion = "2.0.0-SNAPSHOT.051" /** * The distinct version of the Compiler used by other build tools. @@ -81,7 +81,7 @@ object Compiler : Dependency() { * transitive dependencies, this is the version used to build the project itself. */ val dogfoodingVersion: String - private const val fallbackDfVersion = "2.0.0-SNAPSHOT.046" + private const val fallbackDfVersion = "2.0.0-SNAPSHOT.051" /** * The artifact for the Compiler Gradle plugin. diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt index 1243b0452..195a51463 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt @@ -40,7 +40,23 @@ import org.gradle.api.Project class IncrementGuard : Plugin { companion object { + const val taskName = "checkVersionIncrement" + + /** + * Tells whether the version increment must be verified for the given + * GitHub Actions event and the base branch of the pull request. + * + * The version is guarded only for pull requests targeting a default or + * a release-line branch, i.e. a branch with the name ending with `master` + * or `main`. For example: `master`, `main`, `2.x-jdk8-master`, `2.x-jdk8-main`. + */ + internal fun shouldCheckVersion(event: String?, baseBranch: String?): Boolean { + if (event != "pull_request" || baseBranch == null) { + return false + } + return baseBranch.endsWith("master") || baseBranch.endsWith("main") + } } /** @@ -48,11 +64,15 @@ class IncrementGuard : Plugin { * * The task is created anyway, but it is enabled only if: * 1. The project is built on GitHub CI, and - * 2. The job is a pull request. + * 2. The job is a pull request targeting a default (`master` or `main`) or + * a release-line (e.g. `2.x-jdk8-master`) branch. * - * The task only runs on non-master branches on GitHub Actions. - * This is done to prevent unexpected CI fails when re-building `master` multiple times, - * creating git tags, and in other cases that go outside the "usual" development cycle. + * It is the responsibility of a branch which aims to merge into a default + * (or otherwise protected) branch to bump the version. Auxiliary branches do not + * deal with the versions in the release cycle, so pull requests targeting them, + * direct pushes, and tag builds do not run the check. This also prevents unexpected + * CI fails when re-building `master` multiple times, creating git tags, and in other + * cases that go outside the "usual" development cycle. */ override fun apply(target: Project) { val tasks = target.tasks @@ -64,7 +84,8 @@ class IncrementGuard : Plugin { if (!shouldCheckVersion()) { logger.info( - "The build does not represent a GitHub Actions feature branch job, " + + "The build does not represent a GitHub Actions pull request job " + + "targeting a default or a release-line branch, " + "the `checkVersionIncrement` task is disabled." ) this.enabled = false @@ -73,40 +94,19 @@ class IncrementGuard : Plugin { } /** - * Returns `true` if the current build is a GitHub Actions build which represents a push - * to a feature branch. - * - * Returns `false` if the associated reference is not a branch (e.g., a tag) or if it has - * the name which ends with `master` or `main`. + * Returns `true` if the current build is a GitHub Actions build of a pull request + * targeting a default (`master` or `main`) or a release-line branch, + * such as `2.x-jdk8-master`. * - * For example, on the following branches the method would return `false`: - * - * 1. `master`. - * 2. `main`. - * 3. `2.x-jdk8-master`. - * 4. `2.x-jdk8-main`. + * Returns `false` for all other builds, including direct pushes, tag builds, + * and pull requests targeting auxiliary branches. * * @see * List of default environment variables provided for GitHub Actions builds */ private fun shouldCheckVersion(): Boolean { val event = System.getenv("GITHUB_EVENT_NAME") - val reference = System.getenv("GITHUB_REF") - if (event != "push" || reference == null) { - return false - } - val branch = branchName(reference) - return when { - branch == null -> false - branch.endsWith("master") -> false - branch.endsWith("main") -> false - else -> true - } - } - - private fun branchName(gitHubRef: String): String? { - val matches = Regex("refs/heads/(.+)").matchEntire(gitHubRef) - val branch = matches?.let { it.groupValues[1] } - return branch + val baseBranch = System.getenv("GITHUB_BASE_REF") + return shouldCheckVersion(event, baseBranch) } } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/KoverConfig.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/KoverConfig.kt index ed2bd0290..e3bd5b98d 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/KoverConfig.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/KoverConfig.kt @@ -276,7 +276,7 @@ class KoverConfig private constructor( .flatMap { root -> root.walk() .filter { !it.isDirectory } - .flatMap { it.fqnsRelativeTo(root).asSequence() } + .flatMap { it.classNamesIn(root).asSequence() } } .distinct() .toList() @@ -359,7 +359,7 @@ private fun KotlinSourceSet.isMainSourceSet(): Boolean = * * Returns an empty list if this file is not under [root]. */ -private fun File.fqnsRelativeTo(root: File): List { +internal fun File.classNamesIn(root: File): List { if (!startsWith(root)) { return emptyList() } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/SiblingCoverage.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/SiblingCoverage.kt index a680d7994..1c9d1f54d 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/SiblingCoverage.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/SiblingCoverage.kt @@ -26,9 +26,13 @@ package io.spine.gradle.report.coverage +import java.io.File import kotlinx.kover.gradle.plugin.dsl.KoverProjectExtension import org.gradle.api.Project import org.gradle.api.Task +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskCollection +import org.gradle.api.tasks.testing.Test /** * Credits the test coverage produced by the [contributor] module for the classes @@ -37,16 +41,23 @@ import org.gradle.api.Task * Some modules' production classes are exercised only by the tests of a sibling * module — for example, the language-neutral `psi` classes are tested through * the Java-PSI fixtures that live in `psi-java`. Kover's per-module report sees - * only this module's own `test` execution data, so that cross-module coverage is + * only this module's own test execution data, so that cross-module coverage is * otherwise missing from the per-module report (which is what Codecov consumes), * even though the root aggregated report already accounts for it. * * This function adds the [contributor]'s JaCoCo execution data to this project's - * `total` report as an additional binary report. Only this project's classes are - * credited from it — coverage of unrelated classes in the same execution data is - * ignored, because a Kover report is scoped to the owning project's classes. The - * report tasks are wired to run after the contributor's `test` task so the data - * is present when a report is generated. + * `total` report as additional binary reports. Only this project's classes are + * credited from them — coverage of unrelated classes in the same execution data + * is ignored, because a Kover report is scoped to the owning project's classes. + * The report tasks are wired to run after the contributor's JVM test tasks so the + * data is present when a report is generated. + * + * The contributor's JVM test tasks are discovered by type rather than by name, so + * the helper works regardless of the module convention: a `jvm-module` contributes + * through its `test` task, a `kmp-module` through `jvmTest`, and any additional + * JVM test tasks are picked up as well. Non-JVM Kotlin test tasks (`*Native`, + * `*Js`, …) are not of type [Test] and are correctly ignored — Kover instruments + * only JVM test tasks. * * Requires the Kover plugin to be applied to this project. * A cross-project **task** dependency is used, not a project dependency, @@ -54,24 +65,40 @@ import org.gradle.api.Task * already depends on this project. */ fun Project.creditTestCoverageFrom(contributor: Project) { - val execFile = contributor.layout.buildDirectory.file(TEST_EXEC_FILE) + val contributorTests = contributor.tasks.withType(Test::class.java) extensions.configure(KoverProjectExtension::class.java) { reports { total { - additionalBinaryReports.add(execFile.map { it.asFile }) + additionalBinaryReports.addAll(contributor.execFilesOf(contributorTests)) } } } tasks.matching { it.consumesCoverageBinaryReports() }.configureEach { - dependsOn("${contributor.path}:test") + dependsOn(contributorTests) + } +} + +/** + * Lazy `Provider` of the JaCoCo execution-data files produced by [testTasks] + * of this project. + * + * When the coverage engine is pinned to JaCoCo via `useJacoco(...)`, Kover writes + * one binary report per instrumented JVM test task at `build/`[BIN_REPORTS_DIR] + * `/.exec`, so the file name follows the task name. Resolved at + * task-graph time, after the contributor's test tasks have been registered. + */ +private fun Project.execFilesOf(testTasks: TaskCollection): Provider> { + val binReports = layout.buildDirectory.dir(BIN_REPORTS_DIR) + return provider { + testTasks.map { binReports.get().file("${it.name}.exec").asFile } } } /** - * The Kover/JaCoCo execution-data file produced by a module's `test` task when - * the coverage engine is pinned to JaCoCo via `useJacoco(...)`. + * The directory under a module's `build/` where Kover writes the per-test-task + * binary execution-data files. */ -private const val TEST_EXEC_FILE: String = "kover/bin-reports/test.exec" +private const val BIN_REPORTS_DIR: String = "kover/bin-reports" /** * Tells whether this is a Kover task that reads the binary reports and therefore diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/publish/IncrementGuardTest.kt b/buildSrc/src/test/kotlin/io/spine/gradle/publish/IncrementGuardTest.kt new file mode 100644 index 000000000..5633c30d4 --- /dev/null +++ b/buildSrc/src/test/kotlin/io/spine/gradle/publish/IncrementGuardTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.gradle.publish + +import io.kotest.matchers.shouldBe +import io.spine.gradle.publish.IncrementGuard.Companion.shouldCheckVersion +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +@DisplayName("`IncrementGuard` should") +class IncrementGuardTest { + + @Nested + inner class `require the version check` { + + @Test + fun `for pull requests targeting default branches`() { + shouldCheckVersion("pull_request", "master") shouldBe true + shouldCheckVersion("pull_request", "main") shouldBe true + } + + @Test + fun `for pull requests targeting release-line branches`() { + shouldCheckVersion("pull_request", "2.x-jdk8-master") shouldBe true + shouldCheckVersion("pull_request", "2.x-jdk8-main") shouldBe true + } + } + + @Nested + inner class `not require the version check` { + + @Test + fun `for pull requests targeting auxiliary branches`() { + shouldCheckVersion("pull_request", "epic-feature") shouldBe false + shouldCheckVersion("pull_request", "master-fixes") shouldBe false + } + + @Test + fun `for push events`() { + shouldCheckVersion("push", "master") shouldBe false + shouldCheckVersion("push", null) shouldBe false + } + + @Test + fun `for pull request events without a base branch`() { + shouldCheckVersion("pull_request", null) shouldBe false + } + + @Test + fun `outside GitHub Actions`() { + shouldCheckVersion(null, null) shouldBe false + } + } +} diff --git a/config b/config index 93dcf210d..234233e0f 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 93dcf210dc3c93b53cdcf75828a29976dd30f9a0 +Subproject commit 234233e0fa407df296ff4742887723653c3dcc95 diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md index 3df896cd4..c935deb65 100644 --- a/docs/dependencies/dependencies.md +++ b/docs/dependencies/dependencies.md @@ -1,6 +1,6 @@ -# Dependencies of `io.spine.tools:classic-codegen:2.0.0-SNAPSHOT.399` +# Dependencies of `io.spine.tools:classic-codegen:2.0.0-SNAPSHOT.400` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -828,14 +828,14 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 09 19:36:00 WEST 2026** using +This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:gradle-plugin-api:2.0.0-SNAPSHOT.399` +# Dependencies of `io.spine.tools:gradle-plugin-api:2.0.0-SNAPSHOT.400` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.0. @@ -1734,14 +1734,14 @@ This report was generated on **Tue Jun 09 19:36:00 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 09 19:36:00 WEST 2026** using +This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:gradle-plugin-api-test-fixtures:2.0.0-SNAPSHOT.399` +# Dependencies of `io.spine.tools:gradle-plugin-api-test-fixtures:2.0.0-SNAPSHOT.400` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.0. @@ -2212,14 +2212,14 @@ This report was generated on **Tue Jun 09 19:36:00 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 09 19:36:00 WEST 2026** using +This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:gradle-root-plugin:2.0.0-SNAPSHOT.399` +# Dependencies of `io.spine.tools:gradle-root-plugin:2.0.0-SNAPSHOT.400` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -3070,14 +3070,14 @@ This report was generated on **Tue Jun 09 19:36:00 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 09 19:36:00 WEST 2026** using +This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:intellij-platform:2.0.0-SNAPSHOT.399` +# Dependencies of `io.spine.tools:intellij-platform:2.0.0-SNAPSHOT.400` ## Runtime 1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3. @@ -4151,14 +4151,14 @@ This report was generated on **Tue Jun 09 19:36:00 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 09 19:36:00 WEST 2026** using +This report was generated on **Wed Jun 10 18:49:06 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:intellij-platform-java:2.0.0-SNAPSHOT.399` +# Dependencies of `io.spine.tools:intellij-platform-java:2.0.0-SNAPSHOT.400` ## Runtime 1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3. @@ -5930,14 +5930,14 @@ This report was generated on **Tue Jun 09 19:36:00 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 09 19:36:01 WEST 2026** using +This report was generated on **Wed Jun 10 18:49:06 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:jvm-tool-plugins:2.0.0-SNAPSHOT.399` +# Dependencies of `io.spine.tools:jvm-tool-plugins:2.0.0-SNAPSHOT.400` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -6780,14 +6780,14 @@ This report was generated on **Tue Jun 09 19:36:01 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 09 19:36:00 WEST 2026** using +This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:jvm-tools:2.0.0-SNAPSHOT.399` +# Dependencies of `io.spine.tools:jvm-tools:2.0.0-SNAPSHOT.400` ## Runtime 1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.1.0. @@ -7547,14 +7547,14 @@ This report was generated on **Tue Jun 09 19:36:00 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 09 19:36:00 WEST 2026** using +This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:plugin-base:2.0.0-SNAPSHOT.399` +# Dependencies of `io.spine.tools:plugin-base:2.0.0-SNAPSHOT.400` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -8405,14 +8405,14 @@ This report was generated on **Tue Jun 09 19:36:00 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 09 19:36:00 WEST 2026** using +This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:plugin-testlib:2.0.0-SNAPSHOT.399` +# Dependencies of `io.spine.tools:plugin-testlib:2.0.0-SNAPSHOT.400` ## Runtime 1. **Group** : com.google.auto.value. **Name** : auto-value-annotations. **Version** : 1.11.1. @@ -9367,14 +9367,14 @@ This report was generated on **Tue Jun 09 19:36:00 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 09 19:36:00 WEST 2026** using +This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:protobuf-setup-plugins:2.0.0-SNAPSHOT.399` +# Dependencies of `io.spine.tools:protobuf-setup-plugins:2.0.0-SNAPSHOT.400` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -10237,14 +10237,14 @@ This report was generated on **Tue Jun 09 19:36:00 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 09 19:36:00 WEST 2026** using +This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:psi:2.0.0-SNAPSHOT.399` +# Dependencies of `io.spine.tools:psi:2.0.0-SNAPSHOT.400` ## Runtime 1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3. @@ -11345,14 +11345,14 @@ This report was generated on **Tue Jun 09 19:36:00 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 09 19:36:00 WEST 2026** using +This report was generated on **Wed Jun 10 18:49:06 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:psi-java:2.0.0-SNAPSHOT.399` +# Dependencies of `io.spine.tools:psi-java:2.0.0-SNAPSHOT.400` ## Runtime 1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3. @@ -13167,14 +13167,14 @@ This report was generated on **Tue Jun 09 19:36:00 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 09 19:36:01 WEST 2026** using +This report was generated on **Wed Jun 10 18:49:06 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:tool-base:2.0.0-SNAPSHOT.399` +# Dependencies of `io.spine.tools:tool-base:2.0.0-SNAPSHOT.400` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -14054,6 +14054,6 @@ This report was generated on **Tue Jun 09 19:36:01 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 09 19:36:00 WEST 2026** using +This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file diff --git a/docs/dependencies/pom.xml b/docs/dependencies/pom.xml index cf5223281..a7b7e58f0 100644 --- a/docs/dependencies/pom.xml +++ b/docs/dependencies/pom.xml @@ -10,7 +10,7 @@ all modules and does not describe the project structure per-subproject. --> io.spine.tools tool-base -2.0.0-SNAPSHOT.399 +2.0.0-SNAPSHOT.400 2015 diff --git a/gradle.properties b/gradle.properties index 559cd0d15..d9d857b8d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,15 @@ org.gradle.parallel=true # Reuse task outputs from the local build cache. # On CI, `gradle/actions/setup-gradle` persists `caches/build-cache-1` across runs, # so cold builds skip work whose inputs are unchanged. -org.gradle.caching=true +# +# Disabled for now: this repository's own build applies the *published* +# `protobuf-setup-plugins` (see the root `build.gradle.kts` buildscript classpath), +# which does not yet declare `generated/` and `desc.ref` as outputs of +# `generateProto`. With the cache on, a `clean build` restores `generateProto` from +# the cache without re-running its `doLast` actions, leaving the generated code +# missing (e.g., in `classic-codegen`). Re-enable after `ToolBase.version` in +# `buildSrc` points to a version containing the fix. +#org.gradle.caching=true # Dokka plugin eats more memory than usual. Therefore, all builds should have enough. org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -XX:+UseParallelGC -Dfile.encoding=UTF-8 diff --git a/jvm-tool-plugins/src/main/kotlin/io/spine/tools/gradle/jvm/plugin/ArtifactMetaPlugin.kt b/jvm-tool-plugins/src/main/kotlin/io/spine/tools/gradle/jvm/plugin/ArtifactMetaPlugin.kt index b31b2b227..0ba3438ea 100644 --- a/jvm-tool-plugins/src/main/kotlin/io/spine/tools/gradle/jvm/plugin/ArtifactMetaPlugin.kt +++ b/jvm-tool-plugins/src/main/kotlin/io/spine/tools/gradle/jvm/plugin/ArtifactMetaPlugin.kt @@ -186,9 +186,16 @@ public class ArtifactMetaPlugin : Plugin { const val id = "io.spine.artifact-meta" /** - * The name of the directory under the project `build` where + * The path of the directory under the project `build` directory where * the plugin creates its working files. + * + * The plugin claims its own subdirectory under `build/spine` — the root + * working directory shared by all Spine plugins — so that the output of + * the [WriteArtifactMeta] task does not overlap with working directories + * of other Spine plugins, such as `build/spine/compiler`. + * Overlapping outputs would force Gradle to require explicit ordering + * between tasks of otherwise unrelated plugins. */ - const val WORKING_DIR = "spine" + const val WORKING_DIR = "spine/artifact-meta" } } diff --git a/protobuf-setup-plugins/src/main/kotlin/io/spine/tools/protobuf/gradle/plugin/DescriptorSetFilePlugin.kt b/protobuf-setup-plugins/src/main/kotlin/io/spine/tools/protobuf/gradle/plugin/DescriptorSetFilePlugin.kt index 929ad0e92..8731db6a1 100644 --- a/protobuf-setup-plugins/src/main/kotlin/io/spine/tools/protobuf/gradle/plugin/DescriptorSetFilePlugin.kt +++ b/protobuf-setup-plugins/src/main/kotlin/io/spine/tools/protobuf/gradle/plugin/DescriptorSetFilePlugin.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,15 +31,13 @@ import io.spine.code.proto.DescriptorSetReferenceFile import io.spine.tools.code.SourceSetName import io.spine.tools.gradle.task.JavaTaskName import io.spine.tools.protobuf.gradle.descriptorSetFile +import java.io.File import org.gradle.kotlin.dsl.withType import org.gradle.language.jvm.tasks.ProcessResources /** * A Gradle project plugin that configures Protobuf generation tasks to produce * descriptor set files and expose them as resources of the corresponding source set. - * - * This plugin reproduces the behavior of `GenerateProtoTask.setupDescriptorSetFileCreation()` - * defined in this repository's buildSrc utilities, but exposes it as a reusable plugin. */ public class DescriptorSetFilePlugin : ProtobufSetupPlugin() { @@ -78,11 +76,32 @@ public class DescriptorSetFilePlugin : ProtobufSetupPlugin() { task.doLast { DescriptorSetReferenceFile.create(descriptorsDir, descriptorSetFile) } + task.declareReferenceFileOutput(descriptorsDir) task.dependOnProcessResourcesTask() } } +/** + * The name of the [GenerateProtoTask] output property for the descriptor set reference file. + */ +private const val REFERENCE_FILE_PROPERTY = "spineDescriptorSetReferenceFile" + +/** + * Declares the [reference file][DescriptorSetReferenceFile] written in + * the `doLast` action of this task as a task output. + * + * The descriptor set file itself is a declared output of [GenerateProtoTask], + * but the reference file is created as a side effect of the task. + * Unless it is declared as an output, the build cache does not store it, + * and a task restored from the cache leaves the reference file missing, + * so it is not packed into the resources of the source set. + */ +private fun GenerateProtoTask.declareReferenceFileOutput(descriptorsDir: File) { + outputs.file(File(descriptorsDir, DescriptorSetReferenceFile.NAME)) + .withPropertyName(REFERENCE_FILE_PROPERTY) +} + /** * Make the `processResources` task depend on this `GenerateProtoTask`. */ diff --git a/protobuf-setup-plugins/src/main/kotlin/io/spine/tools/protobuf/gradle/plugin/GeneratedSourcePlugin.kt b/protobuf-setup-plugins/src/main/kotlin/io/spine/tools/protobuf/gradle/plugin/GeneratedSourcePlugin.kt index 6be0f0ad6..9307a5165 100644 --- a/protobuf-setup-plugins/src/main/kotlin/io/spine/tools/protobuf/gradle/plugin/GeneratedSourcePlugin.kt +++ b/protobuf-setup-plugins/src/main/kotlin/io/spine/tools/protobuf/gradle/plugin/GeneratedSourcePlugin.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,7 @@ import io.spine.tools.resolve import java.io.File import java.nio.file.Path import org.gradle.api.Project +import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.SourceDirectorySet import org.gradle.api.tasks.SourceSet import org.gradle.plugins.ide.idea.GenerateIdeaModule @@ -72,6 +73,7 @@ public class GeneratedSourcePlugin : ProtobufSetupPlugin(), GeneratedDirectoryCo override fun setup(task: GenerateProtoTask): Unit = with(task) { builtins.maybeCreate("kotlin") configureSourceSetDirs() + declareGeneratedDirOutput() doLast { copyGeneratedFiles() } @@ -102,8 +104,28 @@ private fun GenerateProtoTask.generatedDir(language: String = ""): File { } /** - * Copies files from the Protobuf plugin's output base directory into our `$projectDir/generated` - * directory and removes `com/google` packages to avoid conflicts with library classes. + * The name of the [GenerateProtoTask] output property for the directory + * receiving the copies of the generated files. + */ +private const val GENERATED_DIR_PROPERTY = "spineGeneratedSourcesDir" + +/** + * Declares `$projectDir/generated/` as an output of this task. + * + * The files are copied into the directory by [copyGeneratedFiles] as a side effect + * of the task. Unless the directory is declared as an output, the build cache does + * not store it, and a task restored from the cache leaves the directory missing, + * which fails the compilation tasks consuming the copied sources. + */ +context(_: GeneratedDirectoryContext) +private fun GenerateProtoTask.declareGeneratedDirOutput() { + outputs.dir(generatedDir()) + .withPropertyName(GENERATED_DIR_PROPERTY) +} + +/** + * Copies files from the Protobuf plugin's output base directory into + * our `$projectDir/generated` directory. */ context(_: GeneratedDirectoryContext) private fun GenerateProtoTask.copyGeneratedFiles() { @@ -125,6 +147,11 @@ private object GeneratedSubdir { /** * Exclude directories produced under `$buildDir/generated/(source|sources)/proto` from * both Java and Kotlin source sets and replace them with `$projectDir/generated/...`. + * + * The replacement directories are added with the dependency on this task so that + * the tasks consuming the source sets (e.g., compilation, `sourcesJar`) run after + * the generated code is copied. The dependency carried by the directories excluded + * by this function is severed, so it must be re-established this way. */ @Internal context(_: GeneratedDirectoryContext) @@ -147,25 +174,29 @@ public fun GenerateProtoTask.configureSourceSetDirs() { lang.srcDirs(newSourceDirectories) } + /** Obtains the generated directory for the [language] built by this task. */ + fun generatedSrc(language: String): ConfigurableFileCollection = + project.files(generatedDir(language)).builtBy(this@configureSourceSetDirs) + val sourceSet = sourceSet if (project.hasJava()) { val java = sourceSet.java excludeFor(java) - java.srcDir(generatedDir(JAVA)) + java.srcDir(generatedSrc(JAVA)) // Add the `grpc` directory unconditionally. // We may not have all the `protoc` plugins configured for the task at this time. // So, we cannot check if the `grpc` plugin is enabled. // It is safe to add the directory anyway, because `srcDir()` does not require // the directory to exist. - java.srcDir(generatedDir(GRPC)) + java.srcDir(generatedSrc(GRPC)) } fun SourceDirectorySet.setup() { excludeFor(this@setup) - srcDirs(generatedDir(KOTLIN)) + srcDir(generatedSrc(KOTLIN)) } if (project.hasKotlin()) { diff --git a/protobuf-setup-plugins/src/main/kotlin/io/spine/tools/protobuf/gradle/plugin/Meta.kt b/protobuf-setup-plugins/src/main/kotlin/io/spine/tools/protobuf/gradle/plugin/Meta.kt index ae185752f..9f80ab0fc 100644 --- a/protobuf-setup-plugins/src/main/kotlin/io/spine/tools/protobuf/gradle/plugin/Meta.kt +++ b/protobuf-setup-plugins/src/main/kotlin/io/spine/tools/protobuf/gradle/plugin/Meta.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ package io.spine.tools.protobuf.gradle.plugin +import io.spine.annotation.VisibleForTesting import io.spine.tools.meta.LazyDependency import io.spine.tools.meta.LazyMeta import io.spine.tools.meta.Module @@ -37,10 +38,21 @@ import io.spine.tools.meta.Module */ internal object Meta : LazyMeta(Module("io.spine.tools", "protobuf-setup-plugins")) +/** + * A base class for Protobuf dependencies. + */ +internal abstract class ProtobufDependency { + + /** + * The name of the Maven group to which all Protobuf dependencies belong. + */ + val group = "com.google.protobuf" +} + /** * The dependency on Protobuf Gradle Plugin. */ -internal object ProtobufGradlePlugin { +internal object ProtobufGradlePlugin : ProtobufDependency() { /** * The ID of the plugin. @@ -50,7 +62,7 @@ internal object ProtobufGradlePlugin { /** * This module is used when setting the version of the plugin in tests. */ - private val module = Module("com.google.protobuf", "protobuf-gradle-plugin") + private val module = Module(group, "protobuf-gradle-plugin") val dependency: LazyDependency = LazyDependency(Meta, module) /** @@ -59,8 +71,20 @@ internal object ProtobufGradlePlugin { val version: String = dependency.artifact.version } -internal object ProtobufProtoc { +/** + * The dependency on the Protobuf compiler. + */ +internal object ProtobufProtoc : ProtobufDependency() { - val module = Module("com.google.protobuf", "protoc") + val module = Module(group, "protoc") val dependency: LazyDependency = LazyDependency(Meta, module) } + +/** + * The dependency on the Protobuf Java runtime which we use in tests. + */ +@VisibleForTesting +internal object ProtobufJava : ProtobufDependency() { + val artefact: String + get() = "$group:protobuf-java:${ProtobufProtoc.dependency.artifact.version}" +} diff --git a/protobuf-setup-plugins/src/test/kotlin/io/spine/tools/protobuf/gradle/plugin/BuildCacheSpec.kt b/protobuf-setup-plugins/src/test/kotlin/io/spine/tools/protobuf/gradle/plugin/BuildCacheSpec.kt new file mode 100644 index 000000000..e54ca0e83 --- /dev/null +++ b/protobuf-setup-plugins/src/test/kotlin/io/spine/tools/protobuf/gradle/plugin/BuildCacheSpec.kt @@ -0,0 +1,211 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.tools.protobuf.gradle.plugin + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.spine.code.proto.DescriptorSetReferenceFile +import io.spine.tools.gradle.testing.Gradle +import io.spine.tools.gradle.testing.Gradle.BUILD_SUCCESSFUL +import io.spine.tools.gradle.testing.runGradleBuild +import io.spine.tools.gradle.testing.under +import io.spine.tools.protobuf.gradle.ProtobufTaskName +import java.io.File +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.TaskOutcome.FROM_CACHE +import org.gradle.testkit.runner.TaskOutcome.SUCCESS +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** + * Verifies that the plugins of this module work with the Gradle build cache + * turned on and off. + * + * Both plugins do their work as side effects of the cacheable `generateProto` + * task: [GeneratedSourcePlugin] copies the generated code into + * `$projectDir/generated/`, and [DescriptorSetFilePlugin] creates + * the `desc.ref` file next to the descriptor set file. These files are declared + * as task outputs so that the build cache stores and restores them. Without + * the declarations, a `clean build` with the cache on restores `generateProto` + * from the cache leaving the files missing, and the compilation fails for + * the absent generated code. + */ +@DisplayName("Protobuf setup plugins, when the Gradle build cache is involved, should") +internal class BuildCacheSpec : ProtobufPluginTest() { + + override val group = "cache.test" + override val version = "1.0.0" + + private val buildCacheArg = "--build-cache" + private val generateProto = ProtobufTaskName.generateProto + + private val sampleJava: File + get() = File(generatedJava, "sample/Sample.java") + private val descriptorsDir: File + get() = File(projectDir, "build/descriptors/main") + private val descRef: File + get() = File(descriptorsDir, DescriptorSetReferenceFile.NAME) + private val expectedDescriptorName: String + get() = "${group}_${projectDir.name}_${version}.desc" + + @Test + fun `restore generated code and the reference file from the cache after 'clean'`() { + setupProject() + + // Seed the build cache. + assertCodegenComplete(build(buildCacheArg)) + + // Delete `build/` along with the descriptor files, and the `generated/` directory. + runGradleBuild(projectDir, listOf("clean", buildCacheArg)) + generatedJava.exists() shouldBe false + descRef.exists() shouldBe false + + // The codegen task must be restored from the cache along with the files + // it creates in its `doLast` actions. + val result = build(buildCacheArg) + result.task(generateProto.path())?.outcome shouldBe FROM_CACHE + assertCodegenComplete(result) + + // The restored descriptor files must be packed as resources. + File(projectDir, "build/resources/main/${DescriptorSetReferenceFile.NAME}") + .exists() shouldBe true + File(projectDir, "build/resources/main/$expectedDescriptorName") + .exists() shouldBe true + } + + @Test + fun `regenerate code after 'clean' when the cache is off`() { + setupProject() + + assertCodegenComplete(build()) + runGradleBuild(projectDir, listOf("clean")) + generatedJava.exists() shouldBe false + + // Without the cache the task simply re-executes after `clean`. + val result = build() + result.task(generateProto.path())?.outcome shouldBe SUCCESS + assertCodegenComplete(result) + } + + private fun build(vararg options: String): BuildResult = + runGradleBuild(projectDir, listOf("build") + options) + + /** + * Asserts that the [result] is successful and all the files produced by + * the plugins are in place. + */ + private fun assertCodegenComplete(result: BuildResult) { + result.output shouldContain BUILD_SUCCESSFUL + sampleJava.exists() shouldBe true + descRef.exists() shouldBe true + descRef.readText().trim() shouldBe expectedDescriptorName + File(descriptorsDir, expectedDescriptorName).exists() shouldBe true + } + + /** + * Creates a test project applying both plugins of this module. + * + * The project uses a build cache local to the project directory for hermeticity. + * Its `clean` task deletes the `generated/` directory, mimicking the convention + * of Spine SDK builds. A handwritten Java class refers to the generated code so + * that the compilation fails if the generated code is missing. + */ + private fun setupProject() { + Gradle.settingsFile.under(projectDir).writeText( + """ + buildCache { + local { + directory = File(rootDir, "build-cache") + } + } + """.trimIndent() + ) + + File(protoDir, "sample.proto").writeText( + """ + syntax = "proto3"; + package sample; + message Msg {} + """.trimIndent() + ) + + val javaDir = File(projectDir, "src/main/java/sample") + javaDir.mkdirs() + File(javaDir, "UsesMsg.java").writeText( + """ + package sample; + + /** + * Refers to the generated code so that the compilation fails + * if the code is not generated. + */ + public final class UsesMsg { + + private UsesMsg() { + } + + public static Sample.Msg newMsg() { + return Sample.Msg.getDefaultInstance(); + } + } + """.trimIndent() + ) + + val protobufJava = ProtobufJava.artefact + + Gradle.buildFile.under(projectDir).writeText( + """ + plugins { + id("java") + id("${ProtobufGradlePlugin.id}") version "${ProtobufGradlePlugin.version}" + id("${DescriptorSetFilePlugin.id}") + id("${GeneratedSourcePlugin.id}") + } + + group = "$group" + version = "$version" + + repositories { + mavenCentral() + } + + dependencies { + implementation("$protobufJava") + } + + protobuf { + protoc { artifact = "${ProtobufProtoc.dependency.artifact.coordinates}" } + } + + // Mimic the Spine SDK convention of deleting `generated/` on `clean`. + tasks.named("clean") { + delete("generated") + } + """.trimIndent() + ) + } +} diff --git a/tool-base/src/main/kotlin/io/spine/tools/code/proto/CodeGeneratorRequestWriter.kt b/tool-base/src/main/kotlin/io/spine/tools/code/proto/CodeGeneratorRequestWriter.kt new file mode 100644 index 000000000..722509d21 --- /dev/null +++ b/tool-base/src/main/kotlin/io/spine/tools/code/proto/CodeGeneratorRequestWriter.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.tools.code.proto + +import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest +import io.spine.io.replaceExtension +import io.spine.string.decodeBase64 +import io.spine.type.parse +import io.spine.type.toJson +import java.io.File +import java.io.InputStream +import java.nio.file.StandardOpenOption.CREATE +import java.nio.file.StandardOpenOption.TRUNCATE_EXISTING +import kotlin.io.path.writeBytes + +/** + * Parses a [CodeGeneratorRequest] from given [input] and writes it into + * files in [binary][writeBinary] and [JSON][writeJson] format. + * + * @param input The input stream containing binary version of the request. + */ +public class CodeGeneratorRequestWriter( + private val input: InputStream +) { + /** + * Lazily evaluated [CodeGeneratorRequest] [parsed][parse] from [input] using + * the extension registry with all known custom Protobuf options. + */ + public val request: CodeGeneratorRequest by lazy { + CodeGeneratorRequest::class.parse(input) + } + + /** + * The target file for writing the request in the binary form. + * + * The name of the request is passed as the [parameter][CodeGeneratorRequest.getParameter] of + * the request as a Base64 encoded file path. + */ + public val requestFile: File by lazy { + File(request.parameter.decodeBase64()) + } + + /** + * The path to the request file in JSON format. + * + * The file has the same name as [requestFile] and the extension of `".pb.json"`. + */ + public val requestFileInJson: File by lazy { + requestFile.replaceExtension("pb.json") + } + + /** + * Writes the request into the location specified in [requestFile]. + */ + public fun writeBinary() { + ensureDirectory() + requestFile.toPath().writeBytes(request.toByteArray(), CREATE, TRUNCATE_EXISTING) + } + + /** + * Writes the request in JSON format to the location specified in [requestFileInJson]. + */ + public fun writeJson() { + val json = request.toJson() + ensureDirectory() + requestFileInJson.writeText(json) + } + + private fun ensureDirectory() { + // The parent directory is `null` when the request file is a bare file + // name resolved against the current working directory. + requestFile.parentFile?.mkdirs() + } +} diff --git a/tool-base/src/test/kotlin/io/spine/tools/code/proto/CodeGeneratorRequestWriterSpec.kt b/tool-base/src/test/kotlin/io/spine/tools/code/proto/CodeGeneratorRequestWriterSpec.kt new file mode 100644 index 000000000..21ccfa899 --- /dev/null +++ b/tool-base/src/test/kotlin/io/spine/tools/code/proto/CodeGeneratorRequestWriterSpec.kt @@ -0,0 +1,117 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.tools.code.proto + +import com.google.protobuf.TimestampProto +import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest +import com.google.protobuf.compiler.codeGeneratorRequest +import com.google.protobuf.compiler.version +import io.kotest.matchers.shouldBe +import io.spine.io.replaceExtension +import io.spine.string.toBase64Encoded +import java.io.File +import java.io.InputStream +import java.nio.file.Path +import kotlin.io.path.inputStream +import kotlin.io.path.writeBytes +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +@DisplayName("`CodeGeneratorRequestWriter` should") +internal class CodeGeneratorRequestWriterSpec { + + private lateinit var requestFile: File + private lateinit var writer: CodeGeneratorRequestWriter + private lateinit var input: InputStream + + @BeforeEach + fun prepareInput(@TempDir dir: Path) { + val inputFile = dir.resolve("input.stream") + // Request the file in the directory which does not exist. + requestFile = dir.resolve("nested/request.binbp").toFile() + val request = constructRequest(requestFile.absolutePath.toBase64Encoded()) + inputFile.writeBytes(request.toByteArray()) + input = inputFile.inputStream() + writer = CodeGeneratorRequestWriter(input) + } + + @AfterEach + fun closeInput() { + input.close() + } + + @Test + fun `write binary version of the request`() { + writer.writeBinary() + requestFile.exists() shouldBe true + } + + @Test + fun `write JSON version of the request`() { + writer.writeJson() + requestFile.replaceExtension("pb.json").exists() shouldBe true + } + + @Test + fun `accept a request file given as a bare file name`() { + // A bare file name resolves against the current working directory + // and has no parent directory to create. + val bareName = "bare-request.binpb" + val request = constructRequest(bareName.toBase64Encoded()) + val file = File(bareName) + try { + request.toByteArray().inputStream().use { + CodeGeneratorRequestWriter(it).writeBinary() + } + file.exists() shouldBe true + } finally { + file.delete() + } + } +} + +/** + * Creates a stub instance [CodeGeneratorRequest] initialized with data from [TimestampProto]. + * + * @param parameterValue + * the value to be set to the `parameter` property of the request. + */ +private fun constructRequest(parameterValue: String): CodeGeneratorRequest = + codeGeneratorRequest { + val descr = TimestampProto.getDescriptor() + protoFile += descr.toProto() + fileToGenerate += descr.file.name + compilerVersion = version { + major = 42 + minor = 314 + patch = 271 + } + parameter = parameterValue + } diff --git a/version.gradle.kts b/version.gradle.kts index 1c45a0692..30c92175f 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,4 +24,4 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -val versionToPublish: String by extra("2.0.0-SNAPSHOT.399") +val versionToPublish: String by extra("2.0.0-SNAPSHOT.400")