Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 20 additions & 26 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,20 @@
# CLAUDE.md

Guidance for Claude Code (and other agents) working in this repository.

## What this is

A Java project template: Gradle (Kotlin DSL), Checkstyle, PMD, google-java-format (via Spotless), JaCoCo, SLF4J/Logback, and a unit/integration test split (`src/test` vs `src/int-test`). See `README.md` for the full command reference.

## Conventions to follow

- **Logging: SLF4J, never `System.out`/`System.err`.** Get a logger with `private static final Logger LOG = LoggerFactory.getLogger(ThisClass.class);` and use parameterized messages (`LOG.info("thing happened: {}", value)`), not string concatenation. PMD's `SystemPrintln` rule (bestpractices, unexcluded) will fail the build on raw console I/O in `src/main`.
- **Formatting is not manual.** Don't hand-format Java files or argue with Checkstyle about whitespace/import order — run `./gradlew spotlessApply` and let google-java-format do it. Checkstyle and PMD are deliberately scoped to things the formatter doesn't cover (naming, imports, coding problems, bug patterns); they're not fighting the formatter.
- **Unit vs integration tests are not interchangeable.** `src/test` (the `test` task) is for fast, isolated unit tests. `src/int-test` (the `intTest` task) is for broader/slower tests. Don't add integration-style tests under `src/test`, and don't assume `./gradlew test` alone exercises integration coverage — see the JaCoCo note below.
- **The 80%-line / 70%-branch coverage gate (`jacocoTestCoverageVerification`) is intentionally wired to combined unit + integration execution data** (`executionData(tasks.test.get(), intTestTask.get())` in `build.gradle.kts`). If you add a new source set or test task, JaCoCo's default conventions won't pick it up automatically — you have to wire its exec data in explicitly, the same way, or coverage numbers will silently look wrong (lower than reality) without the gate failing to catch it, or vice versa.
- **PR descriptions must list Jira story keys** per `.github/pull_request_template.md` — that's the org's convention (see the Trident Jira Project Management Playbook), not something to improvise away.

## Known gotchas / do-not-"fix"-without-checking

- **`googleJavaFormat("1.28.0")` is pinned deliberately, not stale.** Spotless 8.9.0's reflective glue adapter (`GoogleJavaFormatFormatterFunc`) breaks against newer google-java-format releases (1.37.0 reproducibly threw `InvocationTargetException` in testing here). If you bump Spotless, re-verify the gjf version still works before bumping it too — don't just grab "latest" for both.
- **`gradle/gradle-daemon-jvm.properties` pins the Gradle daemon itself to JDK 25**, separately from the project's `java.toolchain` (also 25). This was necessary because an ambient JDK 27 on the dev machine caused flaky in-process formatter failures. Don't remove this file assuming the project toolchain setting alone is enough — they serve different purposes (daemon JVM vs. compile/run JVM).
- **PMD's `classpath` is explicitly given the toolchain's `jrt-fs.jar`** (`build.gradle.kts`, near the PMD config) to stop it from guessing "current platform" JDK classes and printing a warning every run. If PMD warnings about aux classpath reappear after a Gradle/PMD version bump, check this block still resolves correctly rather than re-adding `PMD_JAVA_DISABLE_AUX_CLASSPATH_WARNINGS` workarounds (that env var doesn't reliably propagate into PMD's forked worker process anyway — verified in testing).
- **`LoosePackageCoupling` and `GuardLogStatement` are deliberately excluded from PMD's design/bestpractices rules** (`config/pmd/ruleset.xml`) — the former needs explicit package config to be meaningful, the latter is largely redundant once logging is parameterized via SLF4J. Don't silently re-add them without updating the ruleset's comments explaining why they're off.

## Before considering a change "done"

Run `./gradlew check` (compiles, formats-checks, Checkstyle, PMD, unit + integration tests, coverage gate) clean before reporting work finished. CI (`.github/workflows/ci.yml`) runs the same tasks as separate matrix jobs so each shows up as its own PR check.
# Project
templateJavaProject — Java template
Gradle (Kotlin DSL), Java 25 (see `gradle/gradle-daemon-jvm.properties`), JUnit 6, SLF4J/Logback

## Build Commands
- Verify everything: `./gradlew check` (compile, format check, Checkstyle, PMD, unit + integration tests, coverage gate)
- Test: `./gradlew test`
- Integration test: `./gradlew intTest`
- Format: `./gradlew spotlessApply`

## Package Structure
com.example.app — application code (`src/main`)
src/test — unit tests (JUnit 6), run by `test`
src/int-test — integration tests, run by `intTest`, not part of `test`

## Key Conventions
- Logging via SLF4J (`LoggerFactory`) — never `System.out`/`System.err`
- Formatting via Spotless + google-java-format — run `spotlessApply`, don't hand-format
- Coverage gate requires 80% line / 70% branch, combined across unit + integration tests
- PRs must list Jira story keys per `.github/pull_request_template.md`
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ A Java project template with Gradle (Kotlin DSL), Checkstyle, PMD, [google-java-
## Requirements

- The [Gradle Wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html) (`./gradlew`) is checked in — you don't need Gradle installed locally.
- No JDK needs to be pre-installed either: the project targets **Java 25** via a Gradle toolchain, and the `foojay-resolver-convention` plugin (`settings.gradle.kts`) will auto-download a matching JDK if one isn't already on your machine.
- The Gradle **daemon** itself is also pinned to JDK 25, via `gradle/gradle-daemon-jvm.properties` (generated with `./gradlew updateDaemonJvm --jvm-version=25`). This keeps the build tooling (Spotless/google-java-format in particular) running on a JDK it's actually compatible with, regardless of whatever JDK happens to be on your `PATH`.
- No JDK needs to be pre-installed either: the project targets **Java 25**, and the `foojay-resolver-convention` plugin (`settings.gradle.kts`) will auto-download a matching JDK if one isn't already on your machine.
- The Java version is defined in exactly one place: `gradle/gradle-daemon-jvm.properties`. It pins the Gradle **daemon** itself (keeping build tooling like Spotless/google-java-format on a JDK it's actually compatible with, regardless of what's on your `PATH`), and `build.gradle.kts` reads that same file to set the project's `java.toolchain`. To bump the Java version, run `./gradlew updateDaemonJvm --jvm-version=<version>` — don't edit a version number directly in `build.gradle.kts`, there isn't one.

## Project layout

Expand Down
17 changes: 14 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ plugins {
jacoco
checkstyle
pmd
id("com.diffplug.spotless") version "8.9.0"
id("com.diffplug.spotless") version "8.10.4"
}

group = "com.example"
Expand All @@ -13,9 +13,20 @@ repositories {
mavenCentral()
}

val daemonJvmLanguageVersion =
providers
.fileContents(layout.projectDirectory.file("gradle/gradle-daemon-jvm.properties"))
.asText
.map { text ->
val version =
text.lineSequence().firstOrNull { it.startsWith("toolchainVersion=") }?.substringAfter("=")
?: error("toolchainVersion not found in gradle/gradle-daemon-jvm.properties")
JavaLanguageVersion.of(version)
}

java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(25))
languageVersion.set(daemonJvmLanguageVersion)
}
}

Expand Down Expand Up @@ -137,7 +148,7 @@ tasks.withType<Pmd>().configureEach {
spotless {
java {
target("src/*/java/**/*.java")
googleJavaFormat("1.28.0")
googleJavaFormat("1.36.1")
removeUnusedImports()
trimTrailingWhitespace()
endWithNewline()
Expand Down
Loading