외부 리뷰("현재 상태를 유지하기 위한 검증이 너무 많고, 그 검증 자체를
다시 검증하는 구조까지 생겼다")를 설계 문서로 정리하고 코드로 반영한다.
설계·판단 근거는 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>
build-logic
The included build that holds this repository's Gradle conventions. A convention lives here when the same machine code was otherwise copied into more than one place, and the copies could drift apart without any check noticing.
What is here
| Plugin | Owns |
|---|---|
ca.architecture-registry (settings) |
project inclusion, directory mapping, and the parsed registry every other reader shares |
ca.strict-test-lane |
tagged / named / own-source-set Test lanes that cannot pass without executing something |
ca.strict-qualification |
qualification lanes that cannot pass without executing every named class, re-checked against the JUnit XML |
ca.evidence |
the JUnit XML reader and the no-skip / required-class rules built on it |
ca.api-surface |
read-only API surface verification with an explicit, separate update task |
ca.dependency-policy |
declared absences, checked against the resolved graph rather than against a comment |
ca.runtime-membership |
the resolved runtime project closure against the registry's memberships |
ca.platform-module |
what a vendored platform leaf (messaging:*, grpc:*, grpc-advanced:*) is: java-library |
ca.grpc-platform-module |
ca.platform-module plus the module-scope io.grpc:grpc-bom import, for the leaves that already had it |
ca.testkit-publisher was here and is not any more: every leaf with a testkit source set moved to
Gradle's own java-test-fixtures (ADR-BUILD-001).
Plain classes rather than plugins, because settings and projects load plugins through different mechanisms and more than one caller needs each:
| Class | Owns |
|---|---|
dev.caskeleton.buildlogic.ModuleRegistry |
reading and validating config/architecture/modules.json, including which runtime compositions exist |
dev.caskeleton.buildlogic.JUnitEvidence |
one JUnit XML reader, with DOCTYPE processing off and no defaulting of absent counts |
dev.caskeleton.buildlogic.RequiredTestExecution |
the single decision "a test this build names must actually have run", asked by ca.strict-test-lane and by ca.evidence |
dev.caskeleton.buildlogic.JavaPublicTypes |
public top-level types, parsed by javac rather than matched with a regular expression |
What the design named and this build does not have
The remediation design's §10.2 listed eight conventions. Two of them were attempted or assessed and deliberately not built, and the reasons belong next to the code rather than in a review thread.
ca.java-leaf — reverted
Written, measured, reverted. The full reasoning is in src/build.gradle beside the static-analysis
block it would have moved. In short: a recorded decision (feature-static-analysis-quality-contract
D8) already put that baseline in the root subprojects {} block; the block is applied once and
copied nowhere, so extracting it removes no duplication; and build-logic would have to re-declare the
spotless / spotbugs / errorprone / dependency-management coordinates and their versions, which
creates a drift surface where there was none.
A content-level baseline of the resolved analysis configuration — compiler args, encoding, release, Checkstyle tool version and config, SpotBugs effort and report level, for every source set of every project — was captured before the attempt and compared after the revert, because the task graph cannot see a weakened Error Prone flag: the task names are identical either way. The two dumps are identical.
ca.optional-adapter — not warranted
Its stated responsibility was "activation metadata and disabled/on composition contract wiring". Neither half is build machine code in this repository:
- Activation metadata is one registry,
docs/registries/env-keys.yaml, verified by one root task,verifyEnvKeys. There is no per-leaf copy for a convention to deduplicate. - The off invariant and the on fail-closed contract are ordinary tests over the shared
testsource set, gated by the master switch in@ConditionalOnPropertyat runtime. They need no source set, no configuration, and no task of their own.
A plugin here would have to invent state to hold — an optionalAdapter { switch = '...' } block that
no build step reads — and a declaration nothing checks is worse than no declaration, because it reads
like a guarantee.
Testing a convention
cd src
./gradlew -p build-logic test --console=plain
The lane and registry conventions are covered by Gradle TestKit against real builds rather than by
reading the plugin source, because the properties that matter — a lane that discovers nothing fails,
a lane never reports up-to-date, a malformed registry is refused before any project is included — are
runtime behaviour rather than text in a script. Two of those tests exist because the obvious reading
of the Gradle documentation was wrong: failOnNoDiscoveredTests does not fire when a tag filter
matches nothing, and failOnNoMatchingTests does not fire when only some of the named tests are
missing.