refactor(build,ci): 현재 상태 검증을 걷어내고 불변조건만 남기는 검증 표면 축소

외부 리뷰("현재 상태를 유지하기 위한 검증이 너무 많고, 그 검증 자체를
다시 검증하는 구조까지 생겼다")를 설계 문서로 정리하고 코드로 반영한다.
설계·판단 근거는 docs/superpowers/specs/2026-09-16-verification-surface-reduction-design.md.

삭제
- .github/ci-gate-matrix.yml(1,025줄) + verify-gate-matrix.sh(568줄):
  Gradle task graph와 workflow graph에 이미 있는 정보의 3중 복제
- verify-gradle-wrapper.sh(799줄): workflow 바이트 해시 잠금.
  wrapper 검증은 gradle/actions/wrapper-validation(full SHA 핀)에 위임
- DeveloperExperienceContractTest 등의 CI YAML mutation 테스트:
  애플리케이션 test suite가 GitHub Actions YAML 파서를 검증하던 계층 역전
- 문서 drift 파서: verifyReadmeCommands, verifyRunbookReferences,
  verifyDocumentedLeafCount, verifyTestSourceSetRegistry
- 빈 레지스트리를 지키던 커스텀 YAML 파서: verifyTrivyignore,
  verifyQuarantineSunset, flaky-quarantine.yaml
- verifyConfigurationPropertiesProcessor, verifyOneTypePerFile:
  각각 ca.spring-config convention과 Checkstyle OneTopLevelClass가 대체
- 정상 입력으로도 성공할 수 없던 messaging always-fail task
- ModuleRegistry의 JSON 필드 집합 정확 일치, sample-portfolio negative guard

이동
- java/quality/spring 공통 설정을 configure(subprojects) 블록에서
  ca.java-conventions / ca.quality-conventions / ca.java-library /
  ca.spring-library convention plugin으로
- 아키텍처 검증을 ca.architecture로, JPA·messaging qualification을
  gradle/qualification/ 아래로, verifyEnvKeys를 :app-bootstrap 소유로

완화
- Git revision은 releaseCheck·아카이브 생성에서만 요구. 일반 빌드는 SNAPSHOT
- SpotBugs/FindSecBugs는 로컬 check에서 빼고 qualityCheck 레인으로

task 계층
- leaf check는 그 leaf만. architectureCheck / qualityCheck /
  configContractCheck / integrationCheck / ci / releaseCheck로 이름 분리

CI
- _reusable-gradle.yml 신규. checkout + wrapper validation + JDK/캐시 공통화
- fileserver-release.yml -> fileserver-certification.yml (CD가 아니라 certification)
- GitHub Actions = CI + artifact, Argo CD = CD 경계를 docs/ci-cd/boundary.md로 고정

순증감 +3,274 / -7,483.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-09-16 20:33:19 +09:00
co-authored by Claude Opus 5
parent d00c76241c
commit ef947e5bb0
95 changed files with 3284 additions and 7493 deletions
@@ -3,6 +3,9 @@
// The design models the SDK as separate Gradle modules. This repository's fail-closed module
// registry outranks that layout, so the module boundaries are packages under
// dev.caskeleton.adapter.outbound.cache.redis.sdk and RedisSdkModuleBoundaryTest enforces them.
apply plugin: 'ca.spring-library'
apply plugin: 'ca.spring-config'
dependencies {
// Registered edges the semantic port adapters need. The SDK's *main* source imports nothing from
// them today — the semantic cache/session/idempotency/rate-limit adapters that did were removed
@@ -35,7 +38,6 @@ dependencies {
// broken compilation of the SDK's own published API, so it is declared directly.
implementation 'io.projectreactor:reactor-core'
implementation 'org.slf4j:slf4j-api'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
// Deliberately absent:
// org.springframework.data:spring-data-redis — the SDK owns its own typed API and command
+3 -1
View File
@@ -2,6 +2,9 @@
// R2 provider is local-persistent; shared-mounted/NFS and SFTP are not stand-ins or implemented
// capabilities. Its IO path uses only the JDK. Spring Boot autoconfigure supplies explicit,
// disabled-default R1/R2 composition and SLF4J remains the diagnostics API.
apply plugin: 'ca.spring-library'
apply plugin: 'ca.spring-config'
description = 'Outbound adapter: file publication (R1 CSV export, R2 local-persistent) plus the ' + \
'local filesystem content platform behind the HTTP Fileserver'
@@ -18,5 +21,4 @@ dependencies {
// (BOOT-017). The composition root still decides whether to wire it — that part is assembly.
implementation 'io.micrometer:micrometer-core'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
}
@@ -1,4 +1,6 @@
// Shared test code as a Gradle test-fixtures variant — ADR-BUILD-001.
apply plugin: 'ca.spring-library'
apply plugin: 'java-test-fixtures'
// Outbound HTTP Client Platform leaf — see
@@ -17,16 +17,15 @@ import com.tngtech.archunit.core.importer.ImportOption;
* depends on the fixtures, which the fixtures themselves trivially do.
*
* <p>The path moved when this leaf adopted {@code java-test-fixtures} (ADR-BUILD-001) and the rule
* caught it: the exclusion still named {@code /classes/java/testkit/}, so the fixtures were suddenly
* production and the boundary test failed on the first run. That is the check working — an import
* filter that silently stops matching is a rule asserted against the wrong corpus.
* caught it: the exclusion still named {@code /classes/java/testkit/}, so the fixtures were
* suddenly production and the boundary test failed on the first run. That is the check working — an
* import filter that silently stops matching is a rule asserted against the wrong corpus.
*/
public final class PlatformClasses {
private static final ImportOption NOT_THE_FIXTURES_SOURCE_SET =
location ->
!location.contains("/classes/java/testFixtures/")
&& !location.contains("test-fixtures");
!location.contains("/classes/java/testFixtures/") && !location.contains("test-fixtures");
private static final JavaClasses PRODUCTION =
new ClassFileImporter()
+2 -1
View File
@@ -1,5 +1,6 @@
// groovy: compiles the UuidCodec Spock specs under src/test/groovy. See README.
plugins {
id 'ca.spring-library'
// groovy: compiles the UuidCodec Spock specs under src/test/groovy. See README.
id 'groovy'
}
+32 -4
View File
@@ -1,4 +1,7 @@
apply plugin: 'ca.spring-library'
apply plugin: 'ca.spring-config'
dependencies {
implementation project(':application-core')
implementation project(':shared-contract')
@@ -10,7 +13,6 @@ dependencies {
exclude group: 'com.fasterxml.jackson.dataformat', module: 'jackson-dataformat-yaml'
}
implementation 'org.slf4j:slf4j-api'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
}
tasks.withType(Test).configureEach {
systemProperty 'messaging.commonEvidenceSchema',
@@ -24,6 +26,9 @@ configurations.configureEach {
exclude group: 'org.snakeyaml', module: 'snakeyaml-engine'
}
import org.gradle.api.artifacts.MinimalExternalModuleDependency
import org.gradle.api.artifacts.ModuleIdentifier
tasks.register('verifyJsonSchemaRuntimeGraph') {
group = 'verification'
description = 'Verifies the closed Jackson 3 / NetworkNT graph contains no YAML or Jackson 2 runtime.'
@@ -46,16 +51,39 @@ tasks.register('verifyJsonSchemaRuntimeGraph') {
throw new GradleException(
"Messaging JSON runtime contains forbidden Jackson 2/YAML modules: ${forbidden}")
}
// The catalog accessors are Providers of a dependency, not coordinate strings.
//
// This block read `.each { String required -> ... }` over them, so Groovy tried to call the
// closure with a TransformBackedProvider and the task threw
// `No signature of method: doCall() ... (TransformBackedProvider)` before comparing
// anything. It had never passed: the forbidden-module half above ran first and found
// nothing, and then this half failed on its own argument types. `check` reached it, but
// only ever after some earlier failure had already stopped the build.
[
libs.json.schema.validator,
libs.jackson3.core,
libs.jackson3.databind
].each { String required ->
if (!modules.contains(required)) {
].collect { Provider<MinimalExternalModuleDependency> accessor ->
ModuleIdentifier module = accessor.get().module
"${module.group}:${module.name}".toString()
}.each { String requiredModule ->
if (!modules.any { it.startsWith(requiredModule + ':') }) {
throw new GradleException(
"Messaging JSON runtime is missing required locked module ${required}")
"Messaging JSON runtime is missing required module ${requiredModule}; " +
"resolved runtime modules are ${modules.toSorted()}")
}
}
// Module, not module-and-version.
//
// The first working version of this compared the full `group:name:version` string from the
// catalog against the resolved graph, and the gate failed on its first real run: the
// catalog pins tools.jackson.core:jackson-core 3.0.2 while the Jackson 3 BOM resolves
// 3.1.5. That is not drift — it is dependency management doing its job, and this task is
// not the place that decides versions (gradle.lockfile is). What this task owns is the
// shape of the runtime graph: the Jackson 3 + NetworkNT engine present, no YAML engine, no
// Jackson 2 databind. Pinning the version here would have made a BOM patch bump a build
// failure in a leaf that never asked for the version.
// Jackson 3 intentionally retains the 2.x-namespace annotations artifact. It is not a
// Jackson 2 databind/runtime engine and is part of the official Jackson 3 BOM graph.
}
+22 -1
View File
@@ -1,3 +1,6 @@
apply plugin: 'ca.spring-library'
apply plugin: 'ca.spring-config'
dependencies {
implementation project(':application-core')
implementation project(':shared-contract')
@@ -44,7 +47,6 @@ dependencies {
exclude group: 'com.fasterxml.jackson.dataformat', module: 'jackson-dataformat-yaml'
}
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
testImplementation 'io.projectreactor:reactor-test'
}
@@ -58,3 +60,22 @@ dependencyPolicy {
absent 'tools.jackson.dataformat:jackson-dataformat-yaml',
because: 'the Jackson 3 coordinate of the same parser, excluded for the same reason'
}
// The three notification gates run with the leaf they are about.
//
// All three existed and passed for months while nothing ran them, and the cost was measurable the
// first time they were: twenty-nine environment variables bound in application.yml were absent from
// the configuration reference — the whole SMTP relay and all eight key-material purposes — and
// thirteen public types had entered the notification API surface without the reviewed baseline
// recording any of them.
//
// They ran on all 62 leaves once, which reached them 62 times and told the developer who changed
// :domain-core about the notification surface. An API surface baseline and a configuration reference
// for one adapter are that adapter's contract, so they belong to the command a developer runs after
// changing it — and the wiring is declared here, in that leaf, rather than reached into from the
// root. `.github/workflows/notification-platform.yml` also invokes all three by name.
tasks.named('check') {
dependsOn rootProject.tasks.named('verifyNotificationApiSurface')
dependsOn rootProject.tasks.named('verifyNotificationConfiguration')
dependsOn rootProject.tasks.named('verifyNotificationEvidence')
}
@@ -10,6 +10,9 @@
// This sentence used to end "and this repo has no version catalog". That is false, and this file
// disproves it twice below with `libs.archunit.junit5` and `libs.jqwik`. Module scope is a locking
// decision; the catalog just has no awssdk entry.
apply plugin: 'ca.spring-library'
apply plugin: 'ca.spring-config'
description = 'Outbound adapter: object storage (S3/MinIO + local filesystem)'
@@ -43,7 +46,6 @@ dependencies {
implementation 'software.amazon.awssdk:s3'
implementation 'software.amazon.awssdk:netty-nio-client'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
// test-only: Testcontainers MinIO integration test for the S3 backend. Uses the core
// GenericContainer (no dedicated module) so the S3 round-trip runs against a real MinIO when
@@ -1,4 +1,7 @@
// Shared test code as a Gradle test-fixtures variant — ADR-BUILD-001.
apply plugin: 'ca.spring-library'
apply plugin: 'ca.spring-config'
apply plugin: 'java-test-fixtures'
// JPA persistence adapter — merged RDBMS base + PostgreSQL vendor module.
@@ -48,7 +51,6 @@ dependencies {
// driver above. Not `developmentOnly`: local is a deployable profile of this artifact, and the
// vendor selector, not the packaging, decides which driver a deployment loads.
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
// JPA platform observability (design §37). Micrometer's observation API already arrives with
// Spring; the meter registry does not, and the platform's transaction/query/retry metrics need
@@ -355,3 +357,9 @@ apiSurface {
apply from: rootProject.file('gradle/jpa-evidence.gradle')
// The JPA readiness registry describes this platform's lanes and resolves their task paths, so it
// runs with this leaf's `check` rather than with all 62. The task itself is registered by
// gradle/qualification/jpa-qualification.gradle, which the root applies.
tasks.named('check') {
dependsOn rootProject.tasks.named('verifyJpaReadinessRegistry')
}
@@ -1,5 +1,8 @@
// Shared test code as a Gradle test-fixtures variant — ADR-BUILD-001. Applied here rather than
// from the root, the way the GraphQL leaf does: only a leaf that has shared test code needs it.
apply plugin: 'ca.spring-library'
apply plugin: 'ca.spring-config'
apply plugin: 'java-test-fixtures'
// MongoDB Document Persistence Platform leaf — see
@@ -30,7 +33,6 @@ dependencies {
implementation 'io.micrometer:micrometer-core'
implementation 'org.slf4j:slf4j-api'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
// The design's module dependency table is enforced as package rules, so ArchUnit is what keeps
// "packages instead of modules" from meaning "no boundary at all".
@@ -1,5 +1,7 @@
// Shared base for outbound integration adapters: correlation, fail-open dependency
// logging, and the @Configuration seam. Depended on by messaging/cache/notification/httpclient.
apply plugin: 'ca.spring-library'
dependencies {
implementation 'org.springframework.boot:spring-boot-autoconfigure'
implementation 'org.slf4j:slf4j-api'