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 headers) { adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/proxy/ForwardedHeaderSanitizer.java:69: public NormalizedForwardedHeaders normalize(InetAddress peer, Map 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 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: *

{@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> allowedProjectDependencies = registry.modules.collectEntries { module -> 1427: Set 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> allowedProjectDependencies = registry.modules.collectEntries { module -> String moduleName = module.gradlePath.replaceFirst('^:', '') Set allowed = module.allowedDependencies.collect { dependencyId -> registry.byId(dependencyId).gradlePath.replaceFirst('^:', '') }.toSet() [(moduleName): allowed] } doLast { Set declaredModules = subprojects.findAll { it.childProjects.isEmpty() } .collect { it.path.replaceFirst('^:', '') }.toSet() Set governedModules = allowedProjectDependencies.keySet() Set missingFromBuild = governedModules - declaredModules Set 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 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