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>
459 lines
34 KiB
Plaintext
459 lines
34 KiB
Plaintext
Scope adapter-outbound-identifier - negative-space probes
|
|
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
|
generatedAt=2026-08-30T00:56:03+00:00
|
|
|
|
=== 8.1 public-surface reachability: each production type's consumers ===
|
|
--- UuidCodec
|
|
repo-wide (excluding this leaf):
|
|
adapter/outbound/persistence-mongo/src/testkit/java/dev/caskeleton/adapter/outbound/mongo/testkit/mapping/MongoBsonSnapshot.java:16:import org.bson.codecs.UuidCodec;
|
|
adapter/outbound/persistence-mongo/src/testkit/java/dev/caskeleton/adapter/outbound/mongo/testkit/mapping/MongoBsonSnapshot.java:59: CodecRegistries.fromCodecs(new UuidCodec(UuidRepresentation.STANDARD)),
|
|
sample-portfolio/README.md:333:- 36자 canonical UUID 와 PostgreSQL native `uuid`(128비트)를 서로 변환합니다. `UuidCodec` 같은 공용
|
|
in-leaf:
|
|
adapter/outbound/identifier/CLAUDE.md
|
|
adapter/outbound/identifier/README.md
|
|
adapter/outbound/identifier/build.gradle
|
|
adapter/outbound/identifier/src/main/java/dev/caskeleton/adapter/outbound/identifier/UuidCodec.java
|
|
adapter/outbound/identifier/src/test/groovy/dev/caskeleton/adapter/outbound/identifier/UuidCodecSpec.groovy
|
|
--- HmacUserPrincipalPseudonymizer
|
|
repo-wide (excluding this leaf):
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/logging/PseudonymizationConfig.java:3:import dev.caskeleton.adapter.outbound.identifier.HmacUserPrincipalPseudonymizer;
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/logging/PseudonymizationConfig.java:23: return new HmacUserPrincipalPseudonymizer(privacySettings.saltBytes());
|
|
sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/logging/SamplePseudonymizationConfig.java:3:import dev.caskeleton.adapter.outbound.identifier.HmacUserPrincipalPseudonymizer;
|
|
sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/logging/SamplePseudonymizationConfig.java:23: return new HmacUserPrincipalPseudonymizer(privacySettings.saltBytes());
|
|
in-leaf:
|
|
adapter/outbound/identifier/README.md
|
|
adapter/outbound/identifier/src/main/java/dev/caskeleton/adapter/outbound/identifier/HmacUserPrincipalPseudonymizer.java
|
|
adapter/outbound/identifier/src/test/java/dev/caskeleton/adapter/outbound/identifier/HmacUserPrincipalPseudonymizerTest.java
|
|
--- RandomUploadIdentifierFactory
|
|
repo-wide (excluding this leaf):
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverServiceConfiguration.java:4:import dev.caskeleton.adapter.outbound.identifier.RandomUploadIdentifierFactory;
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverServiceConfiguration.java:94: return new RandomUploadIdentifierFactory();
|
|
in-leaf:
|
|
adapter/outbound/identifier/src/main/java/dev/caskeleton/adapter/outbound/identifier/RandomUploadIdentifierFactory.java
|
|
|
|
=== 8.1b who calls each UuidCodec method ===
|
|
$ git grep -rn 'UuidCodec\.' -- . | grep -v '^Binary'
|
|
adapter/outbound/identifier/src/test/groovy/dev/caskeleton/adapter/outbound/identifier/UuidCodecSpec.groovy:11: UuidCodec.normalize(input) == CANONICAL
|
|
adapter/outbound/identifier/src/test/groovy/dev/caskeleton/adapter/outbound/identifier/UuidCodecSpec.groovy:21: UuidCodec.normalize(null) == null
|
|
adapter/outbound/identifier/src/test/groovy/dev/caskeleton/adapter/outbound/identifier/UuidCodecSpec.groovy:26: UuidCodec.normalize("not-a-uuid")
|
|
adapter/outbound/identifier/src/test/groovy/dev/caskeleton/adapter/outbound/identifier/UuidCodecSpec.groovy:34: def uuid = UuidCodec.toUuid(CANONICAL)
|
|
adapter/outbound/identifier/src/test/groovy/dev/caskeleton/adapter/outbound/identifier/UuidCodecSpec.groovy:37: UuidCodec.fromUuid(uuid) == CANONICAL
|
|
exit=0
|
|
|
|
$ git grep -rn 'normalize(\|toUuid(\|fromUuid(' -- . | grep -v '^Binary' | grep -v "^adapter/outbound/identifier/" | head -20
|
|
adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/authz/RolePermissionRegistry.java:32: resolved.put(normalize(role), permissions);
|
|
adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/authz/RolePermissionRegistry.java:44: .map(role -> permissionsByRole.getOrDefault(normalize(role), Set.of()))
|
|
adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/authz/RolePermissionRegistry.java:49: private static String normalize(String role) {
|
|
adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/error/ClientSafeValidationDetails.java:49: return detail(normalize(violation.getPropertyPath()), rule);
|
|
adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/error/ClientSafeValidationDetails.java:53: return detail(normalize(fieldError.getField()), ruleFor(fieldError.getCode()));
|
|
adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/error/ClientSafeValidationDetails.java:69: private static String normalize(Path path) {
|
|
adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/error/ClientSafeValidationDetails.java:79: return names.isEmpty() ? normalize(path.toString()) : String.join(".", names);
|
|
adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/error/ClientSafeValidationDetails.java:82: private static String normalize(String rawPath) {
|
|
adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/proxy/ForwardedHeaderSanitizer.java:53: public NormalizedForwardedHeaders normalize(String peerAddress, Map<String, String> headers) {
|
|
adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/proxy/ForwardedHeaderSanitizer.java:69: public NormalizedForwardedHeaders normalize(InetAddress peer, Map<String, String> headers) {
|
|
adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/proxy/ForwardedHeaderSanitizer.java:70: return normalize(peer == null ? null : peer.getHostAddress(), headers);
|
|
adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/notification/platform/callback/NotificationCallbackMvcControllerTest.java:217: public List<NormalizedProviderEvent> normalize(VerifiedCallback callback) {
|
|
adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/proxy/ForwardedHeaderSanitizerTest.java:26: () -> sanitizer.normalize("203.0.113.10", Map.of("X-Forwarded-Host", "evil.example")))
|
|
adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/proxy/ForwardedHeaderSanitizerTest.java:37: sanitizer.normalize("203.0.113.10", Map.of("X-Forwarded-Host", "attacker-chosen")))
|
|
adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/proxy/ForwardedHeaderSanitizerTest.java:46: sanitizer.normalize(
|
|
adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/proxy/ForwardedHeaderSanitizerTest.java:70: sanitizer.normalize(
|
|
adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/proxy/ForwardedHeaderSanitizerTest.java:94: assertThat(direct.normalize("10.1.2.3", Map.of("X-Forwarded-Host", "evil.example")))
|
|
adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/proxy/ForwardedHeaderSanitizerTest.java:102: assertThat(sanitizer.normalize("203.0.113.10", Map.of("Accept", "application/json")))
|
|
adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/testkit/contract/CrossStackParityTest.java:66: assertThat(normalize(reactive))
|
|
adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/testkit/contract/CrossStackParityTest.java:68: .isEqualTo(normalize(tomcat));
|
|
exit=0
|
|
|
|
=== 8.2 UUID version: what the docs claim vs what is generated ===
|
|
$ grep -n 'UUIDv7\|RFC 9562\|uuid-creator\|f4b6a3' adapter/outbound/identifier/CLAUDE.md adapter/outbound/identifier/README.md adapter/outbound/identifier/build.gradle
|
|
adapter/outbound/identifier/CLAUDE.md:21:- `UuidCodec` — UUID handling on top of the JDK `java.util.UUID` (RFC 9562 UUIDv7):
|
|
adapter/outbound/identifier/CLAUDE.md:32: only `:domain-core` + `com.github.f4b6a3:uuid-creator` are declared in
|
|
adapter/outbound/identifier/README.md:12:없는**(no external-system integration) 능력만 담는다: 식별자 생성/인코딩(UUIDv7), 같은 근거로 clock·
|
|
adapter/outbound/identifier/README.md:19:도메인 무관 UUID 변환 유틸. JDK `java.util.UUID`(RFC 9562 UUIDv7) 위에서 동작한다.
|
|
exit=0
|
|
|
|
$ grep -n 'randomUUID\|time-ordered' adapter/outbound/identifier/src/main/java/dev/caskeleton/adapter/outbound/identifier/RandomUploadIdentifierFactory.java
|
|
12: * strong source rather than a sequence or a timestamp. A time-ordered identifier would be the
|
|
17: * <p>{@link UUID#randomUUID()} is backed by a seeded {@code SecureRandom} and is safe to share
|
|
24: return FileId.of(UUID.randomUUID());
|
|
29: return UploadId.of(UUID.randomUUID());
|
|
exit=0
|
|
|
|
$ git grep -rn 'f4b6a3\|uuid-creator\|UuidCreator' -- . | grep -v '^Binary' | head
|
|
adapter/outbound/identifier/CLAUDE.md:32: only `:domain-core` + `com.github.f4b6a3:uuid-creator` are declared in
|
|
app-bootstrap/gradle.lockfile:33:com.github.f4b6a3:uuid-creator:6.1.1=sampleFixture,testRuntimeClasspath
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:2461: .haveFullyQualifiedName("com.github.f4b6a3.uuid.UuidCreator")
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:2465: + "(UUID.randomUUID / UuidCreator) — inject the domain WorkLogIdFactory port "
|
|
sample-portfolio/README.md:386:- 둘 다 `UuidCreator.getTimeOrderedEpochPlus1()` 을 씁니다 — RFC 9562 UUIDv7(time-ordered)이며 같은
|
|
sample-portfolio/README.md:388:- **여기가 샘플에서 `UuidCreator` 호출이 허용되는 유일한 곳**입니다. `no_uuid_random_in_controller`
|
|
sample-portfolio/build.gradle:52: implementation 'com.github.f4b6a3:uuid-creator:6.1.1'
|
|
sample-portfolio/gradle.lockfile:19:com.github.f4b6a3:uuid-creator:6.1.1=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
|
sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/inbound/web/operation/SampleOperationStore.java:3:import com.github.f4b6a3.uuid.UuidCreator;
|
|
sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/inbound/web/operation/SampleOperationStore.java:27: String id = UuidCreator.getTimeOrderedEpochPlus1().toString();
|
|
exit=0
|
|
|
|
=== 8.2b UuidCodec null handling across the three sibling methods ===
|
|
$ grep -n 'public static' -A 6 adapter/outbound/identifier/src/main/java/dev/caskeleton/adapter/outbound/identifier/UuidCodec.java
|
|
16: public static String normalize(String input) {
|
|
17- if (input == null) {
|
|
18- return null;
|
|
19- }
|
|
20- return UUID.fromString(input).toString();
|
|
21- }
|
|
22-
|
|
23: public static UUID toUuid(String uuidString) {
|
|
24- return UUID.fromString(uuidString);
|
|
25- }
|
|
26-
|
|
27: public static String fromUuid(UUID uuid) {
|
|
28- return uuid.toString();
|
|
29- }
|
|
30-}
|
|
exit=0
|
|
|
|
=== 8.3 declared dependencies: CLAUDE.md claim vs build.gradle vs registry ===
|
|
$ grep -n 'Allowed' -A 4 adapter/outbound/identifier/CLAUDE.md
|
|
29:## Allowed
|
|
30-
|
|
31-- `:application-core`, `:domain-core`, `:shared-contract` (Gradle matrix). Currently
|
|
32- only `:domain-core` + `com.github.f4b6a3:uuid-creator` are declared in
|
|
33- [build.gradle](build.gradle).
|
|
exit=0
|
|
|
|
$ grep -n 'dependencies' -A 6 adapter/outbound/identifier/build.gradle
|
|
6:dependencies {
|
|
7- implementation project(':application-core')
|
|
8-
|
|
9- testImplementation 'org.spockframework:spock-core:2.4-groovy-5.0'
|
|
10-}
|
|
11-
|
|
12-// Pin UTF-8 so non-ASCII (Korean) Spock spec names build on any host. See README.
|
|
exit=0
|
|
|
|
$ grep -n 'allowedProjectDependencies' -A 40 build.gradle | grep -n 'identifier'
|
|
exit=1
|
|
|
|
$ grep -rn 'adapter-identifier\|adapter-outbound-identifier' build.gradle | head
|
|
exit=0
|
|
|
|
=== 8.3b package root: README claim vs actual ===
|
|
$ grep -n 'dev.caskeleton.adapter' adapter/outbound/identifier/README.md adapter/outbound/identifier/CLAUDE.md
|
|
adapter/outbound/identifier/README.md:3:비-IO 인프라 능력(capability) 어댑터 모듈. 패키지 루트: `dev.caskeleton.adapter.identifier`.
|
|
adapter/outbound/identifier/CLAUDE.md:11:Package root: `dev.caskeleton.adapter.outbound.identifier`.
|
|
exit=0
|
|
|
|
$ head -1 adapter/outbound/identifier/src/main/java/dev/caskeleton/adapter/outbound/identifier/UuidCodec.java
|
|
package dev.caskeleton.adapter.outbound.identifier;
|
|
exit=0
|
|
|
|
=== 8.3c Spock/Groovy variant: README claim vs build.gradle ===
|
|
$ grep -n 'Spock\|Groovy\|groovy' adapter/outbound/identifier/README.md | head
|
|
55:### Groovy / Spock (C2 테스트 형태)
|
|
56:순수 값-코덱 동작(`UuidCodec`)은 Groovy/Spock 스펙(`src/test/groovy`)으로 명세한다. core `groovy`
|
|
59:Java(`src/test/java`)로 둔다. Spock 2.4 / Groovy 4.0 variant 를 쓰며, spock-core 가 groovy.jar 를
|
|
60:transitive 로 끌어오므로 data-driven `where:` 스펙에 다른 Groovy 모듈이 필요 없다.
|
|
69:한국어(비-ASCII) Spock 스펙 메서드명은 소스를 UTF-8 로 읽어야만 컴파일·리포팅이 정상이다. 이 모듈이
|
|
exit=0
|
|
|
|
$ grep -n 'spock-core' adapter/outbound/identifier/build.gradle
|
|
9: testImplementation 'org.spockframework:spock-core:2.4-groovy-5.0'
|
|
exit=0
|
|
|
|
=== 8.4 the guards CLAUDE.md names: do they exist? ===
|
|
$ git grep -rn 'identifier_adapter_does_not_depend_on_other_adapters_or_bootstrap' -- . | grep -v '^Binary'
|
|
adapter/outbound/identifier/CLAUDE.md:38: `identifier_adapter_does_not_depend_on_other_adapters_or_bootstrap`;
|
|
exit=0
|
|
|
|
$ ls ../.claude/hooks/ 2>&1
|
|
ls: cannot access '../.claude/hooks/': No such file or directory
|
|
exit=2
|
|
|
|
$ grep -n 'G4' ../.claude/hooks/ca_import_gate.py 2>/dev/null | head
|
|
exit=0
|
|
|
|
=== 8.4b UUID handling elsewhere: is UuidCodec the single path? ===
|
|
$ git grep -rln 'UUID.fromString' -- . | grep -v '^Binary' | grep -v "^adapter/outbound/identifier/" | head -20
|
|
adapter/inbound/graphql/src/main/java/dev/caskeleton/adapter/inbound/graphql/scalar/UuidScalar.java
|
|
adapter/inbound/graphql/src/test/java/dev/caskeleton/adapter/inbound/graphql/scalar/GraphQlScalarContractTest.java
|
|
adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/advanced/codec/WebCodecMapperTest.java
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/JacksonRoutingPlanCodec.java
|
|
adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlIdempotencyClaimRepository.java
|
|
adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/readiness/PostgreSqlInboxCutoffIntegrationTest.java
|
|
adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/readiness/PostgreSqlNotificationIdempotencyRaceIntegrationTest.java
|
|
adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/api/query/SignedJsonCursorCodecTest.java
|
|
adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/query/pagination/MongoCursorValueCodec.java
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/integration/fileserver/FileserverRoundTripContractTest.java
|
|
application-core/src/main/java/dev/caskeleton/application/fileserver/api/FileId.java
|
|
application-core/src/main/java/dev/caskeleton/application/fileserver/api/UploadId.java
|
|
application-core/src/test/java/dev/caskeleton/application/fileserver/api/ValueObjectTest.java
|
|
application-core/src/test/java/dev/caskeleton/application/notification/platform/dispatch/PlatformFakes.java
|
|
application-core/src/test/java/dev/caskeleton/application/notification/platform/dispatch/SyntheticEventFingerprintTest.java
|
|
messaging/messaging-admin-runtime/src/test/java/dev/caskeleton/messaging/admin/runtime/ApprovalForgeryTest.java
|
|
messaging/messaging-admin-runtime/src/test/java/dev/caskeleton/messaging/admin/runtime/ApprovedPlanExecutionTest.java
|
|
messaging/messaging-cloudevents/src/main/java/dev/caskeleton/messaging/cloudevents/DefaultCloudEventMapper.java
|
|
messaging/messaging-cloudevents/src/test/java/dev/caskeleton/messaging/cloudevents/CloudEventMappingTest.java
|
|
messaging/messaging-core-api/src/test/java/dev/caskeleton/messaging/api/CoreValueTypesTest.java
|
|
exit=0
|
|
|
|
$ git grep -rn 'columnDefinition = "uuid"\|@JdbcTypeCode\|SqlTypes.UUID' -- adapter/outbound/persistence-jpa | head -10
|
|
adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/entity/CleanupItemEntity.java:24: @JdbcTypeCode(SqlTypes.UUID)
|
|
adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/entity/CleanupItemEntity.java:28: @JdbcTypeCode(SqlTypes.UUID)
|
|
adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/entity/CleanupItemEntity.java:42: @JdbcTypeCode(SqlTypes.UUID)
|
|
adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/entity/CleanupItemEntity.java:75: @JdbcTypeCode(SqlTypes.UUID)
|
|
adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/entity/FileEntity.java:26: @JdbcTypeCode(SqlTypes.UUID)
|
|
adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/entity/FileEntity.java:56: @JdbcTypeCode(SqlTypes.CHAR)
|
|
adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/entity/QuotaReservationEntity.java:24: @JdbcTypeCode(SqlTypes.UUID)
|
|
adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/entity/RecoveryItemEntity.java:25: @JdbcTypeCode(SqlTypes.UUID)
|
|
adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/entity/RecoveryItemEntity.java:29: @JdbcTypeCode(SqlTypes.UUID)
|
|
adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/entity/UploadSessionEntity.java:30: @JdbcTypeCode(SqlTypes.UUID)
|
|
exit=0
|
|
|
|
|
|
=== 8.4c what actually enforces this leaf's boundary, if not the named rule ===
|
|
$ grep -rn 'allowedProjectDependencies' build.gradle | head
|
|
1416: Map<String, Set<String>> allowedProjectDependencies = registry.modules.collectEntries { module ->
|
|
1427: Set<String> governedModules = allowedProjectDependencies.keySet()
|
|
1445: allowedProjectDependencies.each { moduleName, allowed ->
|
|
exit=0
|
|
|
|
$ grep -rn 'allowedProjectDependencies' -A 60 build.gradle | grep -iE 'identifier|adapter-outbound' | head
|
|
exit=0
|
|
|
|
$ git grep -rn 'identifier' -- app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java | head -20
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:33:import dev.caskeleton.domain.identifier.ResourceId;
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:948: * EXCLUDING the {@code ..adapter.outbound.identifier..} sub-package: UUIDv7 id/event-id factories
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:981: * ..adapter.outbound.identifier..} (UUIDv7 id/event-id generation & codec). Both nest under
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:997: || packageName.contains(".adapter.outbound.identifier");
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:1064: "..adapter.outbound.identifier..",
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:1258: // NOTE: adapter.outbound.identifier AND adapter.outbound.persistence are both nested under
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:1263: // above. Without this, an intra-identifier or intra-persistence reference (a class referencing a
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:1272: .resideOutsideOfPackage("..adapter.outbound.identifier..")
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:1284: "..adapter.outbound.identifier..",
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:1292: .resideInAPackage("..adapter.outbound.identifier..")
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:1308: "..adapter.outbound.identifier.."))))
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:1310: "adapter:outbound:identifier is a non-IO driven adapter (UUIDv7 generation/codec) —"
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:1312: + " (feature-resource-identifier-contract §4 taxonomy).")
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:1367: "outbound adapters (persistence / identifier / cache / httpclient / messaging / "
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:2432: // ---- feature-resource-identifier-contract D17 (decision SSOT: resource-identifier branch) ----
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:2447: + " (feature-resource-identifier-contract D17). JPA @Id UUID columns in"
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:2464: + "(use cases / services) must not generate resource identifiers directly "
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:2466: + "instead (feature-resource-identifier-contract D5/D17). Scope follows the "
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:2481: + "seed an identifier (feature-resource-identifier-contract D9/D17).")
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:2495: + "back to varchar(255) (feature-resource-identifier-contract D17).")
|
|
exit=0
|
|
|
|
$ ls -a ../ | head -20; ls ../.claude 2>&1
|
|
.
|
|
..
|
|
.agents
|
|
.claude
|
|
.codex
|
|
.git
|
|
.gitattributes
|
|
.github
|
|
.gitignore
|
|
.superpowers
|
|
.tool-versions
|
|
.trivyignore.yaml
|
|
.vscode
|
|
AGENTS.md
|
|
CLAUDE.md
|
|
README.md
|
|
docker-compose.dev.yml
|
|
docker-compose.infra.yml
|
|
docker-compose.local.yml
|
|
docker-compose.prod-smoke.yml
|
|
settings.local.json
|
|
exit=0
|
|
|
|
=== 8.4d does anything test UuidCodec's D3/D10 claim outside its own spec? ===
|
|
$ git grep -rn 'D3\b\|D10\b' -- 'adapter/outbound/identifier' ../docs 2>/dev/null | head -12
|
|
../docs/adr/ADR-MONGO-001-platform-boundary.md:28:| D3 Explicit Mongo capability | Native BSON, time series, search/vector, CSFLE/QE, shard-aware operations | Separate capability client |
|
|
../docs/adr/ADR-MONGO-001-platform-boundary.md:36:3. **D3 is not a raw-client escape.** Every call passes a fixed admission order: capability registered
|
|
../docs/mongodb/repository-adaptation.md:103:- D1 / D2 / D3 / D4 exposure planes and the ordered D3 admission sequence (§5).
|
|
../docs/mongodb/repository-adaptation.md:104:- Stable API V1 with `apiStrict=true` on the D1/D2 client generation; D3/D4 on separate generations.
|
|
../docs/mongodb/security-observability.md:62:The native capability gateway (D3) refuses any admin-category command, so there is no path from the
|
|
../docs/mongodb/support-matrix.md:55:| D3 Explicit Mongo capability | Not strict | Native BSON, time series, search/vector, CSFLE/QE, shard-aware operations — each behind a registered capability. |
|
|
../docs/mongodb/support-matrix.md:58:D3 is not a raw-client escape. Every call passes capability registration → database profile →
|
|
../docs/superpowers/plans/2026-08-11-mongodb-document-persistence-platform-implementation-plan.md:7:**Architecture:** `mongodb-core-api`가 framework-free 의미론을 소유하고 Spring Data·imperative·reactive·transaction·query·aggregation·index-schema·change-stream 모듈이 이를 구현한다. 도메인은 Document와 Repository를 직접 소유하며 플랫폼은 범용 CRUD Repository를 만들지 않는다. D1/D2는 Stable API V1 strict client, D3는 승인된 Capability client, D4는 Runtime과 분리된 Admin client를 사용한다.
|
|
../docs/superpowers/plans/2026-08-11-mongodb-document-persistence-platform-implementation-plan.md:22:- D3 Capability client와 D4 Admin client는 별도 권한·설정·모듈이다.
|
|
../docs/superpowers/plans/2026-08-11-mongodb-document-persistence-platform-implementation-plan.md:2948:### Task 38: D3 Native Capability Gateway Guardrail 구현
|
|
../docs/superpowers/plans/2026-08-15-wave2-module-on-path.md:389:### Task D3: Mode-scoped worker lifecycle (NTF-INT-003)
|
|
../docs/superpowers/plans/evidence/2026-08-15-wave2-decisions.md:748:## D3 — INGEST_ONLY was delivering (NTF-INT-003)
|
|
exit=0
|
|
|
|
|
|
=== 8.4e the two guards CLAUDE.md names, resolved accurately ===
|
|
(1) ArchUnit rule name
|
|
$ git grep -rn 'identifier_adapter_does_not_depend_on_other_adapters_or_bootstrap' -- . | grep -v '^Binary'
|
|
adapter/outbound/identifier/CLAUDE.md:38: `identifier_adapter_does_not_depend_on_other_adapters_or_bootstrap`;
|
|
exit=0
|
|
|
|
$ sed -n '1258,1315p' app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java
|
|
// NOTE: adapter.outbound.identifier AND adapter.outbound.persistence are both nested under
|
|
// adapter.outbound (sibling adapters, not top-level packages), so the rules below exclude each
|
|
// one's own package from the side that would otherwise match it against itself — mirroring the
|
|
// resideOutsideOfPackage sibling-exclusion style already used for
|
|
// PERSISTENCE_RDBMS_STAYS_VENDOR_NEUTRAL / PRODUCTION_CODE_DOES_NOT_DEPEND_ON_TEST_FIXTURES
|
|
// above. Without this, an intra-identifier or intra-persistence reference (a class referencing a
|
|
// class in its own nested adapter) would spuriously violate one of the rules.
|
|
|
|
@ArchTest
|
|
static final ArchRule OUTBOUND_ADAPTER_DOES_NOT_DEPEND_ON_WEB_OR_PERSISTENCE_ADAPTERS =
|
|
noClasses()
|
|
.that()
|
|
.resideInAPackage("..adapter.outbound..")
|
|
.and()
|
|
.resideOutsideOfPackage("..adapter.outbound.identifier..")
|
|
// persistence-jpa is itself a nested outbound adapter; exclude its own package from the
|
|
// selector so its intra-references (which land in the ..adapter.outbound.persistence..
|
|
// forbidden target below) do not self-overlap. Generic outbound adapters (messaging,
|
|
// cache, http, ...) are still forbidden from depending on persistence.
|
|
.and()
|
|
.resideOutsideOfPackage("..adapter.outbound.persistence..")
|
|
.should()
|
|
.dependOnClassesThat()
|
|
.resideInAnyPackage(
|
|
"..adapter.inbound.web..",
|
|
"..adapter.outbound.persistence..",
|
|
"..adapter.outbound.identifier..",
|
|
"..bootstrap..")
|
|
.allowEmptyShould(true);
|
|
|
|
@ArchTest
|
|
static final ArchRule IDENTIFIER_ADAPTER_DOES_NOT_DEPEND_ON_OTHER_ADAPTERS_OR_BOOTSTRAP =
|
|
noClasses()
|
|
.that()
|
|
.resideInAPackage("..adapter.outbound.identifier..")
|
|
.should()
|
|
.dependOnClassesThat(
|
|
JavaClass.Predicates.resideInAnyPackage(
|
|
"..adapter.inbound.web..",
|
|
"..adapter.outbound.persistence..",
|
|
"..bootstrap..",
|
|
"org.springframework.data.repository..",
|
|
"org.springframework.data.jpa.repository..",
|
|
"jakarta.persistence..",
|
|
"javax.persistence..",
|
|
"org.hibernate..")
|
|
.or(
|
|
JavaClass.Predicates.resideInAPackage("..adapter.outbound..")
|
|
.and(
|
|
JavaClass.Predicates.resideOutsideOfPackage(
|
|
"..adapter.outbound.identifier.."))))
|
|
.as(
|
|
"adapter:outbound:identifier is a non-IO driven adapter (UUIDv7 generation/codec) —"
|
|
+ " it must not reach into sibling adapters, persistence, or the composition root"
|
|
+ " (feature-resource-identifier-contract §4 taxonomy).")
|
|
.allowEmptyShould(true);
|
|
|
|
// ---- Task 7: explicit inbound/outbound adapter topology (hexagonal driving/driven split) ----
|
|
exit=0
|
|
|
|
(2) the import-gate hook
|
|
$ ls -a ../.claude/
|
|
.
|
|
..
|
|
settings.local.json
|
|
exit=0
|
|
|
|
$ find ../.claude -name '*import*' -o -name '*gate*' | head
|
|
exit=0
|
|
|
|
=== 8.4f the decision document CLAUDE.md/README cite ===
|
|
$ grep -rn 'feature-resource-identifier-contract' -- . ../docs ../.agents 2>/dev/null | grep -v '^Binary' | sed 's/:.*contract/: ...contract/' | sort -u | head -12
|
|
./adapter/outbound/identifier/CLAUDE.md: ...contract §4 taxonomy).
|
|
./app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java: ...contract D17 (decision SSOT: resource-identifier branch) ----
|
|
./app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java: ...contract D17)"));
|
|
./app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java: ...contract D17). JPA @Id UUID columns in"
|
|
./app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java: ...contract D17).")
|
|
./app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java: ...contract D5/D17). Scope follows the "
|
|
./app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java: ...contract D9/D17).")
|
|
./app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java: ...contract §4 taxonomy).")
|
|
./sample-portfolio/src/test/java/dev/caskeleton/sample/portfolio/fixtures/SamplePortfolioFixture.java: ...contract §8/D19).
|
|
exit=0
|
|
|
|
$ find .. -iname '*resource-identifier*' -not -path '*/build/*' -not -path '*/.git/*' | head
|
|
exit=0
|
|
|
|
=== 8.4g how README's allowedProjectDependencies key compares to the real mechanism ===
|
|
$ sed -n '1410,1450p' build.gradle
|
|
// This task used to re-parse the same JSON and re-implement four rules the settings plugin
|
|
// enforces first: file present, module list non-empty, ids unique, allowed-dependency ids
|
|
// resolvable. Two implementations of one rule are two definitions of "valid", and the
|
|
// disagreement only becomes visible on the day one of them is wrong. What is left here is what
|
|
// this task actually owns — comparing the declared edges against the ones Gradle resolved.
|
|
def registry = gradle.moduleRegistry
|
|
Map<String, Set<String>> allowedProjectDependencies = registry.modules.collectEntries { module ->
|
|
String moduleName = module.gradlePath.replaceFirst('^:', '')
|
|
Set<String> allowed = module.allowedDependencies.collect { dependencyId ->
|
|
registry.byId(dependencyId).gradlePath.replaceFirst('^:', '')
|
|
}.toSet()
|
|
[(moduleName): allowed]
|
|
}
|
|
|
|
doLast {
|
|
Set<String> declaredModules = subprojects.findAll { it.childProjects.isEmpty() }
|
|
.collect { it.path.replaceFirst('^:', '') }.toSet()
|
|
Set<String> governedModules = allowedProjectDependencies.keySet()
|
|
Set<String> missingFromBuild = governedModules - declaredModules
|
|
Set<String> missingFromPolicy = declaredModules - governedModules
|
|
|
|
if (!missingFromBuild.isEmpty()) {
|
|
throw new GradleException(
|
|
"Clean Architecture dependency policy references missing Gradle modules ${missingFromBuild}. " +
|
|
"Declared modules are ${declaredModules}."
|
|
)
|
|
}
|
|
|
|
if (!missingFromPolicy.isEmpty()) {
|
|
throw new GradleException(
|
|
"Gradle modules ${missingFromPolicy} are not covered by verifyCleanArchitectureDependencies. " +
|
|
"Add an explicit dependency policy before using them."
|
|
)
|
|
}
|
|
|
|
allowedProjectDependencies.each { moduleName, allowed ->
|
|
Project module = rootProject.project(":${moduleName}")
|
|
Set<String> actual = ['api', 'implementation', 'compileOnly', 'runtimeOnly']
|
|
.collect { configurationName -> module.configurations.findByName(configurationName) }
|
|
.findAll { it != null }
|
|
.collectMany { configuration ->
|
|
exit=0
|
|
|
|
=== EXECUTION PROBE (temporary test class, added / run / removed) ===
|
|
src/test/java/.../IdProbe.java (:adapter:outbound:identifier:test)
|
|
|
|
PROBE normalize("0190bd6e-7c3e-7abc-8def-0123456789ab") -> "0190bd6e-7c3e-7abc-8def-0123456789ab"
|
|
PROBE normalize("0190BD6E-7C3E-7ABC-8DEF-0123456789AB") -> "0190bd6e-7c3e-7abc-8def-0123456789ab"
|
|
PROBE normalize("1-1-1-1-1") -> "00000001-0001-0001-0001-000000000001"
|
|
PROBE normalize("0-0-0-0-0") -> "00000000-0000-0000-0000-000000000000"
|
|
PROBE normalize("1-2-3-4-5") -> "00000001-0002-0003-0004-000000000005"
|
|
PROBE normalize("0190bd6e7c3e7abc8def0123456789ab") -> IllegalArgumentException
|
|
PROBE normalize("0190bd6e-...-0123456789ab-extra") -> IllegalArgumentException
|
|
PROBE normalize(" 0190bd6e-...-0123456789ab ") -> IllegalArgumentException
|
|
PROBE normalize("not-a-uuid") -> IllegalArgumentException
|
|
|
|
PROBE normalize(null) -> null
|
|
PROBE toUuid(null) -> NullPointerException
|
|
PROBE fromUuid(null) -> NullPointerException
|
|
PROBE toUuid("not-a-uuid") -> IllegalArgumentException
|
|
|
|
PROBE newFileId version=4 variant=2
|
|
PROBE newUploadId version=4 variant=2
|
|
PROBE UUID.randomUUID() version=4 (RFC 9562 UUIDv7 would report version=7)
|
|
|
|
Reading: UUID.fromString's non-fast path accepts five dash-separated hex groups of any
|
|
length, so normalize() accepts strings that are not canonical UUIDs and rewrites them into
|
|
canonical-looking ones. The Javadoc and README say it accepts a canonical UUID and raises
|
|
IllegalArgumentException on a malformed one. The Spock spec's single rejection case,
|
|
"not-a-uuid", does not have the dash shape that reaches the lenient path.
|
|
|
|
=== source tree after probe removal ===
|
|
$ git status --short | wc -l
|
|
0
|
|
exit=0
|
|
|
|
$ git status --short
|
|
exit=0
|
|
|