# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Rule #0: the docs are the constraint, not a description Architecture decisions live in **[`../conti-docs/backend/`](../conti-docs/backend/)** (01–13, the single source of truth). This repo's `docs/` is a **copy** of it, so people don't have to switch repos. - **Edit docs in `../conti-docs/backend/`, then sync into `docs/`** (sync commands — including two cross-repo link fixups — are in `docs/README.md`). Editing only one side causes drift. - **When code and docs disagree, change the code, not the doc.** In particular, **never loosen a rule in `architecture-test` just to make code pass** — loosening the rules is exactly what makes this whole structure worthless. - The few places where the scaffolding deliberately deviates from the docs are commented in the code itself. ## Commands ```bash # Everyday check: compile + unit tests + ArchUnit. No Docker needed (~1 min). ./gradlew build -PexcludeIntegration # Full check: includes Testcontainers / WireMock. Needs a working Docker daemon. Run before an MR. ./gradlew build # Single test class / single test method ./gradlew :architecture-test:test --tests "*KotlinSyntaxScanTest*" ./gradlew :domains:identity-store:test --tests "*StoreJpaRepositoryTest*" # Run locally (only MySQL is in Docker; the app runs via Gradle so breakpoints/hot-reload work) docker compose up -d mysql SPRING_PROFILES_ACTIVE=local ./gradlew :bootstrap:bootRun # Swagger: http://localhost:8080/swagger-ui.html # Reset the database docker compose down -v && docker compose up -d mysql ``` Tests tagged `integration` (real MySQL / WireMock) are skipped by `-PexcludeIntegration`. When you touch a repository layer or SQL, **`-PexcludeIntegration` does not exercise what you changed** — run the full build. ## Architecture **Modular monolith**: 14 Gradle modules, one deployable jar. Boundaries are enforced *physically* by the Gradle dependency graph, and *statically* by `architecture-test`. ``` bootstrap the only module with @SpringBootApplication; the only one producing a jar; the only one that applies the Boot plugin platform/platform-* cross-cutting infrastructure (web / security / persistence / observability / integration) domains/* business domains; identity-store is the only fully implemented vertical slice integration/* adapters to external systems (f6-adapter / mini-clients) architecture-test no production code; holds the architecture rules and source-scan tests ``` Dependency rules (enforced at compile time): - **`domains/x` must never depend on `domains/y`** — only on `y-contract` (a read-only contract module containing no Spring Web / JPA). - `domains/*` may depend on `integration/*` and `platform-*`. - Among `platform-*`, a module **may only depend on `platform-web`**. - Layering: `api → application → domain`, with `infrastructure` depended on by both. The `domain` layer **must not reference Spring or JPA at all** — its beans are wired explicitly via `@Bean` in `infrastructure/config/`. Entities live only in the `infrastructure` package; the `api` layer never sees them. **Adding a domain requires wiring 4 places.** Miss one and the symptom is subtle (it compiles and boots, but Flyway doesn't run or the boundary is silently unguarded): 1. `settings.gradle`: add the `include`, and write the module's `build.gradle`. 2. `bootstrap/build.gradle`: add `implementation project(...)`, or it never lands in the jar. 3. `DomainFlywayConfig.DOMAIN_SCHEMAS`: add the schema name, create `db/migration//V1__init.sql` (a Flyway location must not be an empty dir), and add the `CREATE DATABASE` to `docker/mysql/init/01-create-databases.sql`. 4. `ArchitectureRulesTest.domains`: add the package name; `NativeQueryScanTest.ownerByModule`: add `"xxx" to ""`. Package name is the directory name with hyphens stripped (`identity-store` → `identitystore`); the sole exception is `bff-orchestration` → `bff`. ## architecture-test: three test classes, three mechanisms Know what's in here before changing production code, or you'll hit a red build and not know why: | Test class | Mechanism | What it catches | | --- | --- | --- | | `ArchitectureRulesTest` | ArchUnit over **bytecode** | layer directions, Entity placement, field injection, `java.util.Date`, util-class naming, etc. | | `NativeQueryScanTest` | **reads source text** | a native SQL string naming another domain's schema — schema names are string literals, invisible in bytecode | | `KotlinSyntaxScanTest` | **reads source text** | `!!` / `lateinit` / `suspend` / `kotlinx.coroutines` / RxJava / Reactor / `data class *Entity` | **Two traps in the source-scan tests** (you only see them by reading the code): - They locate sources with **relative paths** (`Path.of("../domains")`), relying on Gradle running them with the module dir as working directory. **If you add a new top-level source root, you must add it to `KotlinSyntaxScanTest.productionSources()`** — otherwise the new code is never checked. - `productionSources()` contains `filter { Files.exists(it) }`: **if the path is wrong it silently scans zero files and reports green.** When writing a test like this, confirm it actually reads files by planting a deliberate violation and checking that it goes red — don't trust a green alone. ### Kotlin coding constraints (see `13-kotlin-style.md`) `!!` / `lateinit` / coroutines / RxJava / Reactor / `data class` entities / any `XxxUtils|Util|Helper|Common` class **are build gates** — violating them fails `test` and therefore `build`. They are not suggestions. Two things that are **deliberately not automated**: - `companion object` restricted to legitimate uses — left to review (all 14 existing uses are legitimate; a hard rule would only cause false positives). - **Entities must be proxyable by Hibernate — currently NOT enforced (known gap)**: the root `build.gradle` does **not** apply `kotlin("plugin.jpa")`, so entities are final, Hibernate cannot proxy them, and `fetch = LAZY` **silently degrades to eager**. This hasn't blown up only because there are currently zero lazy associations. Read the "已知缺口(2026-09)" (known gap) section of `13-kotlin-style.md` before adding anything like `@ManyToOne(fetch = LAZY)`. ## Hard version constraints | Component | Version | Constraint | | --- | --- | --- | | Gradle | `9.5.0` (wrapper) | **Two-sided**: the Boot 4.1 plugin needs 8.14+ or 9.x, while KGP 2.4.10's fully-supported ceiling is 9.5.0. Bumping either Boot or Kotlin means recomputing the intersection. | | Kotlin | `2.4.10` | | | Java | `21` (pinned via toolchain) | | | Spring Boot / Cloud | `4.1.0` / `2025.1.2` | | - Version alignment always goes through Gradle's native **`platform()` BOM; `io.spring.dependency-management` is deliberately not used** (that plugin historically breaks on every Gradle major). - **Gradle 9 no longer bundles the JUnit Platform launcher** — deleting `testRuntimeOnly 'org.junit.platform:junit-platform-launcher'` from the root `build.gradle` makes every `test` task fail outright. - **Boot 4 moved packages**: `@DataJpaTest` → `org.springframework.boot.data.jpa.test.autoconfigure`, `@AutoConfigureTestDatabase` → `org.springframework.boot.jdbc.test.autoconfigure` (both need `spring-boot-starter-data-jpa-test`); `spring-boot-starter-aop` is now `spring-boot-starter-aspectj`. ## Database conventions - One domain, one database (in MySQL, schema ≡ database), so `DomainFlywayConfig` starts one Flyway instance per entry in `DOMAIN_SCHEMAS`. - **An already-executed Flyway script must never be edited** (checksum mismatch → startup fails with `Validate failed`; this applies to the dev environment too). To change something, add a higher-versioned script. - **Every migration must be forward-compatible**: `maxUnavailable: 0` means old and new pods share one database during a rolling update, so dropping or altering a column requires expand-contract across two releases. - **Store-scoped queries must use `StoreContextHolder.currentStoreId()`, never a `storeId` request parameter** — that parameter is the standard entry point for an authorization-bypass query. - Seed data (`LocalSeedDataConfig`) **exists only under the `local` profile and must never be written into a Flyway script**. ## Deployment and misc - `local` on your own machine (Gradle); `dev` on internal k3s (`./scripts/deploy-dev.sh `, connect VPN first); `uat`/`prod` on Azure AKS (protected tag → manual Promote in GitLab). - **Images don't compile code**: the `Dockerfile` consumes the jar produced by the CI `validate` stage, so the bytes deployed are the bytes that were tested. - **Real secrets are in no file in this repo**; `k8s/secret-uat.yaml` holds only `__injected_by_pipeline__` placeholders. - The `lint` (ktlint/detekt) and coverage jobs are currently **left commented out** in `.gitlab-ci.yml` — the plugins aren't wired up, so enabling them would go red immediately. `.editorconfig` already reserves the `ij_kotlin_*` section.