chore: 加 CLAUDE.md,忽略 agent 工作目录

CLAUDE.md(英文)给后续 AI 用:文档是约束来源而非描述、构建与测试命令、
architecture-test 三个测试类的不同机制(含源码扫描测试路径写错时静默扫 0 个
文件报绿的坑)、以及版本基线的硬约束。

.gitignore 忽略 .claude / .agents。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Guangfei.Zhao
2026-09-30 16:01:51 +08:00
co-authored by Claude Code
parent aca7487be2
commit d78fcfc202
2 changed files with 163 additions and 0 deletions
+158
View File
@@ -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/<schema>/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 "<schema>"`.
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 <sha>`, 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.