한 커밋인 이유: src/build.gradle 안에서 ca.testkit-publisher 플러그인 제거와
게이트 수정이 얽혀 있다. 플러그인 적용부만 빼면 web·websocket·persistence-jpa·
persistence-mongo·app-bootstrap 이 사라진 testkitPublisher() 와 *Testkit
컨피규레이션을 계속 참조해 설정 단계에서 빌드가 죽는다. 파일 단위로 나눌 수 없다.
1) testkit 소스셋 → Gradle 표준 java-test-fixtures 이관
web, websocket, persistence-jpa, persistence-mongo, httpclient, graphql 과
이들의 testkit 컨피규레이션을 소비하던 app-bootstrap.
자체 제작 ca.testkit-publisher.gradle 77줄이 사라진다.
2) 실행되지 않거나 실패할 수 없던 빌드 게이트 정상화 (E등급)
- strict-test-lane 의 실행 카운터가 skip 을 실행으로 세던 것 수정.
전부 skip 인 레인은 이제 실패한다 (회귀 테스트 2건 추가)
- public-path 스냅샷이 gitignore 된 src/.env 를 읽던 것을
config/security.yml 의 바인딩 기본값으로 교체
- verifyEnvKeys 가 build/ 산출물을 소스로 읽어 삭제된 키를 사용 중으로
오판하던 것 수정 (입력 4,637 → 4,630 파일)
- jpa-evidence 가 git 실패를 "워크트리 깨끗함"으로 읽던 것을 fail-closed 로
- notification-evidence 의 Grade 열 탐지를 헤더 기준으로 교체 +
표 부재 시 fail-closed
- spring70CompatibilityTest 가 레인을 복제하며 잃은 fail-closed 복구
(태스크명 유지 — 워크플로 3곳과 gate-matrix 린트 무손상)
- 메시징 R2 스켈레톤 주변의 도달 불가 검증 45줄을 MSG-015 명시적 실패로 교체
3) git 에 없던 빌드 필수 파일 추적
- src/gradle/libs.versions.toml — src/build.gradle 이 9곳에서 참조하는데
추적되지 않아 깨끗한 체크아웃에서 설정이 실패했다
- app-bootstrap config/*.yml 15개 — application.yml 이 전부 import 한다.
하드코딩된 시크릿은 없고 값은 secret://environment/APP_* 참조다
4) 진행 중이던 구현 작업 반영 (redis/idempotency 구성, startup 검증,
아키텍처 테스트 클래스, notification 콜백 레지스트리 등)
검증:
- 깨끗한 체크아웃에서 ./gradlew help 통과
- :app-bootstrap:test --tests 'dev.caskeleton.bootstrap.architecture.*'
→ 20개 클래스 174 tests, 실패 0, 스킵 0 (이전에는 0개 실행)
미해결: verifyOneTypePerFile 은 손대지 않았다(Checkstyle 로 교체 권고).
B/C/D 등급 100여 건과 CI 단계 분리는 별도 작업 —
docs/superpowers/plans/2026-09-16-ci-stage-separation.md 참고.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
가장 큰 것: 문서화된 아키텍처 게이트가 규칙을 하나도 실행하지 않았다.
워크플로 7곳과 CLAUDE.md 가 --tests '*CleanArchitectureTest' 를 지정했으나
그 이름의 클래스는 존재하지 않는다. 4곳은 매칭 0건으로 하드 실패하고,
3곳은 다른 필터와 병기돼 아키텍처 규칙 0개를 돌고 초록으로 통과했다.
필터를 패키지 글롭 dev.caskeleton.bootstrap.architecture.* 로 교체했다.
*ArchitectureTest 글롭은 20개 중 12개만 잡고 ArchRuleDiscoveryContractTest 등
8개를 놓치므로 쓰지 않았다.
그 외:
- ci-gate-matrix 의 release_blocking 이 강제되지 않아 trivy-fs 가 빨개도
release-gate 가 초록이던 것을 실제 의존으로 연결
- build-logic TestKit 이 어떤 CI 에서도 돌지 않던 것을 ci-quality-gates 에 연결
- jpa-next-* 3개, object-storage, fileserver-pr 의 실패할 수 없거나
트리거되지 않던 잡 정리
- 릴리스 태그 네임스페이스 분열로 v* 태그가 web·websocket 게이트를
건너뛰던 것 수정
- 워크플로 SHA 잠금 28개 재생성 (verify-gradle-wrapper.sh)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- CI 단계 분리 계획 추가 (docs/superpowers/plans/2026-09-16-ci-stage-separation.md).
빌드·CI 레이어 전수 리뷰 133건의 결론과 Track A/B/C 작업 순서를 담는다.
- public-path 보안 기준선을 실제 배포 기본값(/v1/healthcheck)으로 재생성.
이전 값은 gitignore 된 src/.env 에서 유래해 재현이 불가능했다.
- 진행 중이던 ADR·리뷰·테스트 전략 문서 반영, 대체된 grpc 계획 문서 제거.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`./gradlew check` failed on main before this change. Both failures came from the
notification platform and neither was reachable from the graphql merge.
- `application-core:checkstyleMain` — ProviderSubmissionResult switches over
AttemptConfirmation and covers all three constants, so the switch is exhaustive,
but checkstyle's MissingSwitchDefault does not model exhaustive arrow switches.
Add the repository's existing idiom (`default -> throw new IllegalStateException`),
the same shape SwitchNotificationWriterOwnershipCommand and DefaultCleanupService
already use. The branch stays unreachable; it exists to satisfy the linter and to
fail loudly if the enum ever grows a constant.
- `adapter:inbound:web:spotbugsMain` — SPRING_CSRF_PROTECTION_DISABLED x2 on
CallbackMvcSecurityConfiguration. Provider callbacks are inbound webhooks: an
external provider POSTs to /internal/notification/callbacks/**, so it can never
carry a CSRF token, and the chain is SESSION-STATELESS with no ambient cookie auth
for CSRF to protect. Authenticity comes from the provider signature the callback
package verifies (failure -> CallbackValidationException -> 400), not from
permitAll(). Register it in the spotbugs exclude filter scoped to that exact class,
matching the existing narrow-exception style; every other CSRF disable stays
reportable.
Verified on the merged tree by replaying the CI jobs locally:
- quality-gates: `check verifyPublicPathSnapshot verifyDependencyLocks
--warning-mode=fail --no-daemon` -> BUILD SUCCESSFUL (13m47s)
- sample-off, redis-sdk, jpa-candidate-evidence, gate-matrix-lint,
conditionalTransportQualification (graphql 8 / grpc 15 / websocket 5 /
composition 1, zero skips)
- notification-platform architecture gate (CleanArchitectureTest +
NotificationArchitectureTest)
- runtime: `:app-bootstrap:bootRun` against the compose-local database started and
served GET /api/healthcheck 200; actuator/health and /api/worklogs answered 401,
so the authentication boundary is live.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
main 통합으로 messaging 플랫폼이 24개 leaf 를 modules.json 에 등록한 것이 드러났다
(현재 총 43개 leaf). 따라서 "레지스트리는 정확히 19개로 고정되어 있고 확장하면 게이트가
깨진다"는 기존 서술은 사실과 다르다.
- 레지스트리는 확장 가능하며, 자매 플랫폼 messaging 은 정반대 패턴(leaf 등록)을 택했다.
- graphql 의 sub-package 매핑은 "레지스트리가 닫혀서"가 아니라 "GraphQL 표면은 하나의
인바운드 전송 경계이고 그 내부 분할을 레포 전역 SSOT 까지 올리지 않는다"는 선택으로
다시 서술한다.
- 두 패턴이 공존하므로 통일 여부는 미결 아키텍처 결정으로 명시한다. 모듈 레코드는 그대로
leaf 명세로 승격 가능한 형태라 분해 비용은 낮게 유지된다.
루트 CLAUDE.md 도 여전히 "exactly 19 leaf identities" 라고 적혀 있으나 이는 messaging 머지에서
비롯된 선행 불일치이므로 이 커밋 범위 밖으로 두고 보고한다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
설계 문서(specs/2026-08-12-graphql-api-execution-platform-design.md)와 두 실행 계획서에
선언된 create path 전량을 adapter-inbound-graphql leaf 안에 구현한다.
- 계획서 main 클래스 322개 전량, Task별 테스트 클래스 67개(Stable 48 + Advanced 19) 전량.
- 설계서의 Stable 16 + Advanced 12 "Gradle 모듈"은 modules.json 이 19개 leaf 정체성을
소유하므로 bounded sub-package 로 매핑한다(선례: httpclient leaf). 모듈 경계는 문서가
아니라 GraphQlStableModule/GraphQlAdvancedModule 값 선언 + GraphQlModuleBoundaryTest 의
실제 소스 스캔으로 기계 검증한다.
- architecture/ 규칙은 리플렉션 + 단순명 매칭으로 구현한다. 인바운드 어댑터가 자신이
금지하는 jakarta.persistence/spring-tx 에 의존해야 검사할 수 있다면 본말전도이기 때문.
- 부분 실패는 HTTP 200 + partial data, 요청 실패는 4xx. GraphQL over HTTP 초안 status 294 는
의도적으로 미채택(초안 변경이 클라이언트를 깨뜨리므로).
- 요청 단위 DB 트랜잭션을 열지 않는다. 커서는 HMAC 서명된 버전 있는 keyset(상수 시간 비교).
- DataLoader 는 요청 스코프, 캐시 키는 actor/tenant sha256 지문으로 격리.
- Advanced capability 는 전부 기본 비활성. EXPERIMENTAL 등급은 명시 승인 없이 production
활성화가 거부된다.
- spring-webflux 는 compileOnly(runtimeClasspath 제외) — MVC 배치가 WebFlux 런타임을
물려받지 않도록. lockfile 이 스코프 제한을 고정.
- graphqlPerformanceTest 는 성능 태그가 0개면 실패한다. failOnNoDiscoveredTests 는 태그
필터로 0건이 된 경우를 잡지 못해(Gradle 9.0.0 실측) 결과 검사를 추가했다. 증거 부재를
통과로 위장하지 않기 위한 fail-closed.
검증: graphqlStableTest 404 / graphqlContractTest 9 / graphqlAdvancedTest 141 tests,
:adapter:inbound:graphql:check, verifyCleanArchitectureDependencies,
CleanArchitectureTest, verifyConfigurationPropertiesProcessor, verifyEnvKeys,
verifyPublicPathSnapshot 전부 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`spotbugsTest` and `spotbugsSampleOffTest` on :app-bootstrap fail on DM_GC in
NotificationObservationTest, which predates this branch — the file arrives from
701ba67 and this branch never touched it. The merge only surfaces it.
The call is intentional and cannot be removed without removing the assertion:
Micrometer holds gauge referents weakly, so a gauge whose source object is
collected reports NaN from then on, and provoking a collection is the only way
to show the notification metrics do not have that defect.
Scoped to the one method by class and method name, per the filter's own rule
that entries be narrow — a System.gc() anywhere else stays reportable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Checkstyle and SpotBugs run in the module's `check` task, not in `test`, so
these only surfaced once the platform was verified against the merged tree.
- MissingSwitchDefault on the admin runtime-state switch. The enum is
exhaustive, so the default is unreachable today; it throws rather than
falling through, so a state added later fails loudly instead of silently
leaving the runtime in whatever state it already had.
- ConstantName on the two audit loggers: the checkstyle pattern allows
`log`, `logger` or UPPER_SNAKE, and this class needs two named sinks.
- DMI_RANDOM_USED_ONLY_ONCE in three Web Push fixtures. A fresh SecureRandom
per call re-seeds from the OS every time, which on a constrained CI runner
can block on entropy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implements the Stable and Experimental JPA persistence platform designs
against real PostgreSQL, adapted to this repository's fail-closed 19-leaf
registry.
The design models the platform as 25 Gradle projects. `src/settings.gradle`
throws unless the registry holds exactly 19 leaves, so the plan's modules
become packages inside `:adapter:outbound:persistence-jpa` (starter in
`:app-bootstrap`, testkit in its own source set). The full mapping, the
renames this repository's naming gate required, and every deliberate
substitution are recorded in `docs/jpa/repository-adaptation.md`.
Seven Docker-backed lanes replace the plan's seven JVM test suites. Each
fails closed: a lane that discovers nothing, or a container that cannot
start, is an error rather than a skip.
Three defects the contracts found against a real server:
- `CommitFailureClassifier` treated only SQLSTATE 40003, class 08, and
transport breaks as completion-unknown. A backend terminated mid-commit
reports 57P01, and the commit record may already be in the WAL — so a
possibly-committed transaction could be re-run. 57P01/57P02/57P03 now
classify as completion-unknown.
- `SchemaTenantMigrationOrchestrator` recorded `MigrateResult`'s target
version, which is empty for a tenant already current, reporting migrated
tenants as unmigrated during a partial rollout. It now reads the applied
version back from the tenant's schema history.
- `JpaStreamExecutor` checked only the declared return type for reactive
publishers, and `RegisteredPostgreSqlCopyLoader` passed the COPY timeout
to `SET`, which is parsed before parameter binding.
`JpaModuleBoundaryTest` enforces the plan's module map as package rules;
`verifyCleanArchitectureDependencies` governs edges between leaves and
cannot see these. Its first assertion is that the import is non-empty,
because every rule under it is a `noClasses()` rule and would pass
vacuously on an empty import.
Verified: 128 container tests across all seven lanes, 1183 unit tests,
`:adapter:outbound:persistence-jpa:check`, `:app-bootstrap:check`,
`verifyCleanArchitectureDependencies`, `verifyOneTypePerFile`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Maps the 31-module plan onto the registry's 19 leaves as packages; the two
edges the registry forbids (provider->httpclient, inbox->messaging) are
replaced by application-owned ports. See docs/notification/module-mapping.md.
Acceptance is not delivery: ProviderSubmissionResult refuses to carry a
delivery outcome, and AMBIGUOUS is a first-class terminal state that blocks
automatic retry and fallback until reconciliation resolves it.
Providers: SES (SigV4 + SNS callback), Twilio (X-Twilio-Signature +
reconciliation), FCM (FID-primary batch), APNs, Web Push (RFC 8030/8291/8292),
SMTP and webhook. Contact points are AES-256-GCM encrypted with a separate
HMAC lookup fingerprint; nothing raw reaches a log, metric tag or exception.
Dispatch commits the attempt row, calls the provider with no transaction open,
then records the outcome; the durable queue uses FOR UPDATE SKIP LOCKED.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brings in the mongodb-superpowers-package implementation (Stable Tasks 1-50,
Advanced Tasks 1-15) as packages inside the registered leaf
:adapter:outbound:persistence-mongo, with the design's module dependency table
enforced by ArchUnit.
Shared build files are untouched by this branch: src/build.gradle,
src/settings.gradle, config/architecture/modules.json and
app-bootstrap/build.gradle are all unchanged, so this merge does not move the
19-leaf registry and does not collide with the other platform branches still in
flight.
Verified before merging: scripts/verify-mongodb-platform.sh reports 9 lanes,
0 skipped, 0 failed, every evidence category produced.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implements the mongodb-superpowers-package design: Stable Tasks 1-50 and
Advanced Tasks 1-15.
The design assumes 19 Stable + 12 Advanced Gradle projects under
modules/mongodb*. This repository's fail-closed registry declares exactly 19
leaf identities, so those modules become package boundaries inside the
registered leaf :adapter:outbound:persistence-mongo, with the design's module
dependency table enforced by ten ArchUnit rules. The mapping and every
deviation are recorded in docs/mongodb/repository-adaptation.md.
Contract highlights, all enforced by tests rather than convention:
- Transaction body retry and commit retry are separate loops. A new session per
body attempt; commit-only retry on an unknown commit. The body is never
replayed after a commit ambiguity, so a failover cannot become a duplicate.
- MongoExecutionOutcome keeps both ambiguous outcomes distinct from success and
failure, and MongoFailureContext records only the design-permitted fields.
- Failure classification reads server error labels before numeric codes.
- BSON representations come from a pinned manifest, never a library default,
and a golden type-signature gate fails on any drift.
- Index and validator changes go through the manifest and the admin plane;
metadata ownership gates every drop.
- Every Advanced capability refuses construction unless its flag is enabled.
Verified against real servers, not only unit tests. Running the lanes for the
first time exposed four defects that a green `check` had hidden:
- Four release lanes passed while executing zero tests; the gate now counts
executed tests per lane and fails on zero.
- The "single replica set" fixture was a standalone, because Testcontainers 2.x
needs withReplicaSet(); its test only asserted a connection string.
- The three-node fixture was three independent clusters, so no election could
occur, and awaitNewPrimary() compared against the post-stop primary.
- The migration lease checked modifiedCount, so a same-millisecond refresh read
as a lost lease.
scripts/verify-mongodb-platform.sh now reports:
9 lanes, 0 skipped, 0 failed, every evidence category produced.
scripts/verify-mongodb-advanced.sh reports NOT PROMOTABLE: actual-topology
evidence (real sharded cluster, real KMS, real target deployment) is
unobtainable here, so it is named rather than assumed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The review found one defect shape repeated across the platform: surfaces
that were declared, bound, and documented, but that nothing read. An
operator configuring fullUrlRecording, bodyLogging, retry.policy,
validatedDnsPinning, timeout.dns, or any of ten declared metric names got a
guarantee the code never delivered. Every such surface is now in exactly one
of three states -- wired for real, rejected at startup, or registered in a
test-enforced gap list with its reason. No silent no-ops remain.
P0:
- Activate the platform from bootstrap behind app.httpclient.enabled, with a
single auto-configuration importing the nine child configurations.
- Give the platform a strict, repository-level ENV contract: 74 leaf fields
derived from the settings record tree, unknown APP_HTTPCLIENT_* rejected.
- Route typed HTTP service clients through the call kernel via
KernelHttpExchangeAdapter, so they stop bypassing platform policy.
- Pin dynamic-target DNS resolution to the socket for the life of a call,
closing the resolve-then-connect TOCTOU / rebinding window.
- Actually transmit the idempotency key, and make retry eligibility depend on
transmission rather than on merely holding one.
- Reject reactive authentication and reactive redirect at startup instead of
declaring support that does not function.
- Fix the Reactor-only Stable contract row so the lane stops failing.
- Stop advertising HTTP/3 on a transport that negotiates HTTP/1.
P1 covers execution and retry accounting, redirect security (per-hop target
guarding, sensitive-header stripping, 303 body handling), runtime rotation
and transport resource ownership keyed by generation, dynamic-target
hardening (subdomain matching, global-unicast classification, strict CIDR
parsing), protocol intent, pool and timeout wiring, streaming and body
limits, observability parity, and OAuth single-flight refresh on a bounded
pool with a bounded wait.
P2 covers configuration and documentation drift, the Gradle check wiring for
the four hermetic lanes, and the CI gate matrix.
Two test-quality defects surfaced while closing these: the HTTP/2 stream
saturation test ran against cleartext HTTP/1.1 while asserting nothing about
the protocol, and an OAuth contention test slept on a latch that could fire
before the callers it meant to observe. Both now assert what their names
claim.
Verification run: :adapter:outbound:httpclient:check and :app-bootstrap:check
(checkstyle, spotless, spotbugs, and the four hermetic lanes),
verifyCleanArchitectureDependencies, verifyEnvKeys, verifyOneTypePerFile,
verifyDependencyLocks, the documentation and gate-matrix verifiers, and the
performance lane against a real TLS+ALPN HTTP/2 server.
Not executed, and tracked rather than claimed: Docker/Toxiproxy fault
injection, JMH, a real QUIC/HTTP3 server, a real Spring Framework 6.2
distribution (now a delegated-pending gate), live OAuth/TLS/proxy/DNS
integration, and a whole-repository check.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Snapshot of the in-flight state that already existed, identically, in both
this worktree and the main checkout before this session began: the initial
HTTP Client platform implementation (previously untracked), the redis-lab
removal, and the JPA / object-storage / notification integration work.
Kept separate from this session's HTTP Client review response, which lands
in the following commit, so the two bodies of work stay reviewable apart.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>