From d78fcfc202e2b810a1a5f5363da328ccdb4eb2e8 Mon Sep 17 00:00:00 2001 From: "Guangfei.Zhao" Date: Wed, 30 Sep 2026 16:01:51 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=8A=A0=20CLAUDE.md=EF=BC=8C?= =?UTF-8?q?=E5=BF=BD=E7=95=A5=20agent=20=E5=B7=A5=E4=BD=9C=E7=9B=AE?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLAUDE.md(英文)给后续 AI 用:文档是约束来源而非描述、构建与测试命令、 architecture-test 三个测试类的不同机制(含源码扫描测试路径写错时静默扫 0 个 文件报绿的坑)、以及版本基线的硬约束。 .gitignore 忽略 .claude / .agents。 Co-Authored-By: Claude Code --- .gitignore | 5 ++ CLAUDE.md | 158 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 CLAUDE.md diff --git a/.gitignore b/.gitignore index fc1406f..8d9efdc 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,8 @@ Thumbs.db application-local-override.yml .env *.env.local + + +# Agents +.claude +.agents diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5260085 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,158 @@ +# 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.