The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
12 KiB
kind, slug, title, topic, project, status, sourceRevision, rootTreeNode, evidenceCapturedOn, body, assets, evidence, source
| kind | slug | title | topic | project | status | sourceRevision | rootTreeNode | evidenceCapturedOn | body | assets | evidence | source | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CASE | a19-f022-messagingpublicsurfacecontracttest | 공개 표면 계약 시험이 가족 밖 app-bootstrap 에 있다 | messaging-and-outbox | clean-architecture-backend-template | 게시 전 | 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 | case:a19-f022-messagingpublicsurfacecontracttest | 2026-09-04 | case-a19-f022-messagingpublicsurfacecontracttest.body.md |
|
|
|
공개 표면 계약 시험이 가족 밖 app-bootstrap 에 있다
지침이 이 규칙을 붙든다고 지목한 MessagingPublicSurfaceContractTest 가 가족 트리가 아니라 src/app-bootstrap/src/test 에 있다. 원문은 이것을 자동 검증이 없는 사례로 읽었는데, 경로 필터가 없는 ci-quality-gates.yml 이 모든 pull_request 에서 ./gradlew check 를 돌리고 그 안에 이 시험이 들어 있다.
관계
- 아무도 돌리지 않는 레인의 게이트는 마지막으로 돌린 사람이 본 것을 보고한다 원문이 이 사례를 그 규칙으로 읽었다. 그런데 이 시험은 경로 필터 없는 워크플로에 실려 PR 마다 새로 도므로, 인용된 문장이 겨냥한 상태가 아니다.
- 문서 계약 테스트의 단언 경계 밖에 발견된 드리프트 세 건이 전부 있었다 둘 다 문서가 어떤 규칙을 시험이 지킨다고 적었는데, 그 시험이 실제로 붙드는 범위가 문서를 읽은 사람이 기대하는 범위보다 좁다. 저 기록은 단언 여덟 개 밖의 드리프트이고, 여기는 가족 태스크가 고르지 않는 소스 트리다.
문제
가족 지침은 :39 에서 이 규칙을 붙드는 것이 문서가 아니라 MessagingPublicSurfaceContractTest 라고 적는다.
그 시험이 어느 트리에 있고 어떤 경로로 실행되는지 확인했다.
결론
MessagingPublicSurfaceContractTest 가 놓인 트리는 src/app-bootstrap/src/test 이고 패키지는 dev.caskeleton.bootstrap.contract.messaging 이다. src/messaging 아래 24 개 트리에는 그 이름이 없다.
담긴 시험은 셋이고 @Tag 는 0 개다. 하나는 api 선언 누락을, 하나는 검사가 헛도는 경우를, 하나는 broker SDK 두 개의 컴파일 클래스패스 유출을 각각 잡는다.
가족 태스크는 이 클래스를 고르지 않는다. :messaging:messaging-kafka:test 에 이름을 넘기면 시험을 찾지 못하고 빌드가 실패한다.
가족 경로에서 도는 messaging-certification.yml 도 이 시험을 돌리지 않는다. 그 워크플로의 gradle 호출은 Kafka 인증 증거 검증 태스크 하나뿐이다.
그러나 원문이 읽은 것과 달리 자동 경로가 있다. ci-quality-gates.yml 은 paths 필터 없이 모든 pull_request 에서 돌고 :50 에서 ./gradlew check 를 돌린다. check 가 모든 레인을 덮지는 않는다는 경고가 그 아래 주석에 있어 이 프로젝트만 따로 확인했다. dry-run 그래프에 :app-bootstrap:test 가 있고, 그 태스크로 실행하면 test/ 아래에 tests=3 failures=0 이 남는다.
판정은 P3 이고 원문과 같다. 다만 근거가 다르다. 원문은 이 계약을 자동으로 검증하는 경로가 없다는 것을 들었는데, 검증은 매 PR 에서 돈다. 남는 것은 가족 태스크에 이 시험이 없어 로컬에서 위반을 보지 못한다는 것이다.
검증 환경
Gradle : 9.0.0 확인 방식 : 지침 문장 원문 확인, 시험 클래스의 소스 트리·패키지·태그 확인과 단언 메서드 전수, 가족 test 트리 계수, 가족 경로 워크플로의 트리거 전부와 실행 명령 확인, 경로 필터 없는 워크플로의 트리거·job 조건·실행 명령과 그 아래 주석 확인, :app-bootstrap:check 의 태스크 그래프 dry-run, 루트 test 규약의 제외 태그 확인, 그 시험을 이름으로 지정한 실제 실행과 결과 XML 계수, 가족 태스크에 같은 이름을 넘긴 실행 소스 수정 : x
재현 조건
- 가족 지침에서 공개 표면 규칙을 붙든다고 적은 문장과 그 시험이 무엇을 대조하는지 문장 끝까지 읽는다.
- 그 클래스를 찾아 소스 트리와 패키지를 확인하고, 가족 트리에 동명 파일이 있는지 센다.
- 그 파일의 @Tag 와 @Disabled 를 세고 단언 메서드를 전부 읽는다.
- 가족 경로에서 도는 워크플로의 트리거를 전부 읽고 실행 명령을 확인한다.
- 경로 필터가 없는 워크플로를 찾고 job 조건과 실행 명령을 읽는다.
- 그 명령 주변 주석에서 check 의 포괄 범위에 대한 경고가 있는지 본다.
- check 를 dry-run 해 그 프로젝트의 test 가 그래프에 있는지 확인한다.
- 루트 규약이 test 에 거는 제외 태그를 읽는다.
- 그 시험을 이름으로 지정해 실제로 돌리고 결과 XML 의 디렉터리와 계수를 읽는다.
- 같은 이름을 가족 태스크에 넘겨 돌린다.
본문
src/messaging/CLAUDE.md:39 는 의존성 노출 규칙을 문서가 아니라 MessagingPublicSurfaceContractTest 가 붙들고 있다고 적는다. 그 클래스는 src/messaging 아래에 없다.
지침이 지목한 시험과 그 트리
:::evidence key="a19-f022-messagingpublicsurfacecontracttest" alt="저장소 루트에서 돌린 정적 검색과 gradle 실행을 합친 출력 76줄. src/messaging/CLAUDE.md 의 36번부터 44번 줄이 원문 그대로 실려 규칙과 그 시험이 무엇을 대조하는지가 문장 끝까지 보인다. 이어서 그 클래스가 app-bootstrap 의 test 트리에 있고 src/messaging 아래 동명 파일이 0 개이며 가족 test 트리가 24 개라는 것, 그 파일의 @Test 가 3 이고 @Tag 와 @Disabled 가 0 이라는 것과 세 메서드 이름이 나온다. messaging-certification 워크플로의 트리거 세 가지가 경로와 cron 과 수동 실행까지 모두 나오고 그것이 돌리는 gradle 명령이 Kafka 인증 증거 검증이라는 것이 보인다. ci-quality-gates 는 on 블록 1번부터 9번 줄까지 실려 paths 필터가 0 건이라는 계수가 붙고, job 정의와 check 를 돌리는 step, 그리고 그 바로 아래 check 가 graphqlStableTest 에 의존하지 않는다고 적은 주석 다섯 줄이 함께 나온다. 마지막으로 app-bootstrap 의 check 를 dry-run 한 결과에 test 가 들어 있는 것, 루트 규약이 거는 유일한 필터, 그 시험을 이름으로 골라 실제로 돌린 결과가 결과 디렉터리 test 에 시험 3 개 실패 0 으로 나온 것, 같은 이름을 가족 태스크로 고르면 시험을 찾지 못하고 빌드가 실패하는 것이 나온다." caption="지침 원문 · 시험 클래스의 트리와 태그와 세 단언 · 가족 워크플로의 트리거 전부와 실행 명령 · 경로 필터 0 인 워크플로와 check 가 모든 시험을 덮지 않는다는 주석 · check 그래프에 있는 test · 이름으로 고른 실행 결과 3개 · 가족 태스크는 찾지 못함 — 76줄 · exit 0" zoom="true" :::
CLAUDE.md:39~:43 은 이 시험이 leaf 의 production source 에서 public·protected 시그니처에 등장하는 vendor 라이브러리를 뽑아 그 leaf 의 build.gradle 이 api 로 선언했는지 대조하고, starter 의 api closure 에 broker client 둘이 들어오지 않는 것도 함께 본다고 적는다.
클래스는 src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/contract/messaging/ 에 있다. 가족 test 트리는 24 개인데 그중 어디에도 같은 이름의 파일이 없다.
@Test 는 셋이다. vendorTypesInPublicSignaturesAreDeclaredApi(:73)가 공개 시그니처의 vendor 타입이 api 로 선언됐는지 보고, theScanIsNotVacuous(:94)가 그 검사가 아무것도 못 찾은 채로 통과하는 경우를 막고, neitherBrokerSdkReachesAnAdoptersCompileClasspath(:114)가 두 broker SDK 가 채택자의 컴파일 클래스패스에 닿지 않는 것을 본다.
가족만 바꾸고 가족 태스크만 돌릴 때
:messaging:*:test 에 이 클래스 이름을 넘겨 봤다. No tests found for given includes 로 빌드가 실패한다.
가족 경로에서 도는 워크플로는 messaging-certification.yml 이다. :16~:19 가 src/messaging/** 와 자기 워크플로 파일에서 돌게 하고 :20~:22 가 주 1회 cron 과 수동 실행을 더한다. :52 가 돌리는 것은 :messaging:messaging-kafka:verifyMessagingCertificationEvidence 다. Kafka 인증 증거이지 이 계약이 아니다.
ci-quality-gates.yml 은 경로 필터 없이 매 PR 에서 check 를 돌린다
:3~:9 의 on 블록은 pull_request 와 main 푸시와 수동 실행이고 paths 가 0 건이다. :20 의 quality-gates job 에 조건이 없고, :49~:50 이 working-directory: src 에서 ./gradlew check verifyPublicPathSnapshot verifyDependencyLocks 를 돌린다.
check 가 모든 시험 태스크를 덮는다고 가정할 수는 없다. 바로 아래 :51~:55 주석이 check 는 graphqlStableTest 에 의존하지 않으며 그래서 그 레인의 가드가 CI 에서 아무것도 지키지 못했다고 적는다. 그래서 이 프로젝트에 대해 직접 확인했다.
:app-bootstrap:check 의 태스크 그래프에 :app-bootstrap:test 가 있다. 루트 규약(src/build.gradle:549~:553)이 test 에 거는 필터는 excludeTags 'quarantine' 하나이고 이 클래스에는 @Tag 가 0 개다.
그리고 이름으로 골라 실제로 돌렸다. BUILD SUCCESSFUL 이고 결과가 test/ 디렉터리에 tests=3 failures=0 skipped=0 으로 남는다. 기본 test 가 이 시험을 고른다.
원문과 갈리는 자리
원문은 결과를 둘로 적었다. 가족 태스크만 돌리면 검증되지 않는다는 것과 가족 경로 워크플로가 Kafka 인증 레인이라는 것이다. 둘 다 맞다.
원문은 그 둘에서 이 사례가 모듈 18 §4.1c 의 문장 — 아무도 지역에서 돌리지 않는 레인의 붉은 게이트는 마지막으로 돌린 사람이 본 것을 보고한다 — 과 같은 형태라고 읽었다. 그 문장은 여기에 붙지 않는다. ci-quality-gates.yml 이 경로 필터 없이 매 PR 에서 check 를 돌리고, 그 안에 이 시험이 들어 있다. 이 게이트가 보고하는 것은 마지막으로 돌린 사람이 본 것이 아니라 그 PR 에서 새로 돈 결과다.
api·implementation 분리가 이 가족의 정책이고 위반이 이 가족의 build.gradle 에서 난다는 서술은 맞다. 다른 것은 검증의 유무가 아니라 그 검증이 실행되는 시점이다. 가족 태스크에는 이 시험이 없고 ci-quality-gates.yml 의 check 에만 있다.
확인하지 못한 것
가족 leaf 에 api 누락을 심어 :messaging:*:test 가 초록으로 끝나는 것을 재현하지 않았다. 그 태스크가 이 클래스 이름을 찾지 못한다는 것까지 실행으로 확인했다.
GitHub 러너에서 ./gradlew check 를 돌리지 않았다. 워크플로가 그 명령을 돌린다는 것, check 의 그래프에 :app-bootstrap:test 가 있다는 것, 그 태스크가 이 시험을 고른다는 것을 각각 확인해 이었다.
워크플로 스물여덟 개의 트리거를 전수로 조사하지 않았다. 이 계약과 관련된 둘을 읽었다.
워크플로 스물여덟 개의 트리거를 전수로 조사하지 않았다. 이 계약과 관련된 둘을 읽었다.