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:
co-authored by
Claude Code
parent
aca7487be2
commit
d78fcfc202
@@ -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.
|
||||
Reference in New Issue
Block a user