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>
234 lines
19 KiB
Markdown
234 lines
19 KiB
Markdown
# 07 · adapter-outbound-identifier
|
||
|
||
|
||
## SSOT identity — 2026-08-31 재검증
|
||
|
||
- registered leaf id: `adapter-outbound-identifier`
|
||
- canonical state `analysisFile`: `analysis/07-adapter-outbound-identifier.md` (이 문서) — 이 leaf의 단일 SSOT
|
||
- source path: `src/adapter/outbound/identifier` · Gradle `:adapter:outbound:identifier`
|
||
- registry `allowed_dependencies`: `["domain-core", "application-core"]`
|
||
- registry `runtime_memberships`: `["app-bootstrap", "sample-portfolio"]`
|
||
- coverage ledger: `FULL_READ` **10** / `STRUCTURAL_ONLY` **0** / `EXCLUDED` **0** / `UNCLASSIFIED` **0**
|
||
- 최초 분석 revision `a24ece9c` → 재검증 revision `21234e38` · 이 리프의 변경 파일 **0**
|
||
- 재검증 증거: `EVD-333`(소스 드리프트 0), `EVD-334`(lane 재실행)
|
||
|
||
> 재검증이 확인한 것은 대상이 움직이지 않았다는 사실이지, 아래 서술이 옳다는 보증이 아니다.
|
||
> 이번 사이클에서 코드에 대고 다시 확인한 항목은 이 문서의 검증 절과 위 증거가 가리키는 범위다.
|
||
|
||
---
|
||
> 상태: COMPLETE
|
||
> revision: `a24ece9cf797f7ea647e33bf846b115208ed1ba5`
|
||
> 경로: `src/adapter/outbound/identifier` · Gradle: `:adapter:outbound:identifier`
|
||
|
||
## 0. Denominator와 coverage ledger
|
||
|
||
이 leaf는 tracked file **10개**다. 하위 범위로 나눌 크기가 아니라 한 범위로 처리한다.
|
||
|
||
| 구분 | 파일 | LOC | 상태 |
|
||
|---|---|---|---|
|
||
| governance (`CLAUDE.md`, `README.md`, `build.gradle`, `gradle.lockfile`) | 4 | 297 | FULL_READ |
|
||
| main Java (`HmacUserPrincipalPseudonymizer`, `RandomUploadIdentifierFactory`, `UuidCodec`, `package-info`) | 4 | 114 | FULL_READ |
|
||
| test Java (`HmacUserPrincipalPseudonymizerTest`) | 1 | 112 | FULL_READ |
|
||
| test Groovy (`UuidCodecSpec`) | 1 | 39 | FULL_READ |
|
||
| **합계** | **10** | **562** | **10 / 10 FULL_READ** |
|
||
|
||
structural-only 0 · excluded 0 · unclassified 0.
|
||
|
||
manifest: `evidence/raw/139-identifier-module-inventory.txt`.
|
||
probe(정적 + 실행): `evidence/raw/140-identifier-negative-space-probes.txt`.
|
||
|
||
레지스트리 항목:
|
||
|
||
```json
|
||
{ "id": "adapter-outbound-identifier",
|
||
"gradle_path": ":adapter:outbound:identifier",
|
||
"allowed_dependencies": ["domain-core", "application-core"],
|
||
"runtime_memberships": ["app-bootstrap", "sample-portfolio"] }
|
||
```
|
||
|
||
## 1. 이 모듈이 존재하는 이유
|
||
|
||
CLAUDE.md와 README가 같은 논거를 편다: **외부 시스템 연동이 없는(non-IO) 인프라 능력**만 모아 두어, `adapter-outbound`가 문서화된 의미("외부 HTTP / messaging / cache / notifications")를 유지하게 한다는 것이다.
|
||
|
||
> Kept out of `adapter-outbound` on purpose: a UUID id/codec capability is infrastructure, not an outbound integration point.
|
||
|
||
그 논거의 예시로 드는 능력이 **UUID id/codec**이다. §3에서 보듯 그 능력에는 production 소비자가 없고, 실제로 배선돼 도는 것은 나머지 둘(가명화, 업로드 식별자)이다.
|
||
|
||
## 2. Confirmed — `HmacUserPrincipalPseudonymizer`는 이 leaf에서 가장 잘 만들어진 부분이다
|
||
|
||
`UserPrincipalPseudonymizerPort`(application-core)의 유일한 구현이고, `app-bootstrap`과 `sample-portfolio`가 각자 싱글톤 빈으로 배선한다(`140-...` §8.1). 결정들이 코드와 문서 양쪽에 맞물려 있다.
|
||
|
||
- **thread safety를 타입이 아니라 수명으로 푼다.** `Mac`은 thread-safe가 아니므로 `pseudonymize` 호출마다 새로 만든다 — 그래서 공유 싱글톤으로 안전하다. 주석이 그 이유를 그 자리에 적는다.
|
||
- **salt를 스스로 조달하지 않는다.** `APP_PRIVACY_PSEUDONYMIZATION_SALT`에서 `app-bootstrap`이 공급하고(분류 secret, 90일 회전), 생성자는 null/빈 배열을 거부하며 거부 메시지가 그 환경변수 이름을 그대로 말한다.
|
||
- **방어적 복사.** `salt.clone()`으로 호출자 배열을 붙들지 않는다.
|
||
- **도달 불가 예외를 정직하게 감싼다.** `HmacSHA256`은 JCA 필수 알고리즘이라 `NoSuchAlgorithmException`·`InvalidKeyException`은 사실상 도달 불가이고, 호출부에 checked exception 잡음을 남기지 않으려 `IllegalStateException`으로 감싸며 주석이 "this should never happen on a compliant JDK"라고 적는다.
|
||
- **Spring-free.** 어노테이션이 없고 빈 생성은 composition root 책임이다.
|
||
|
||
test 11개가 경계를 실제로 나눠 덮는다 — 생성자 가드 2, null/blank/empty 입력 3, 같은 salt의 결정성(같은 인스턴스·다른 인스턴스) 2, salt 민감도 1, 단방향성 2(출력≠입력, 출력이 입력을 부분문자열로 포함하지 않음), 출력 포맷 1(`^[0-9a-f]{64}$`). 값 하나를 고정하는 golden test가 아니라 **성질**을 검사한다.
|
||
|
||
`RandomUploadIdentifierFactory`의 판단도 기록해 둘 만하다. 파일 식별자가 공개 핸들이므로 시퀀스나 타임스탬프가 아니라 암호학적 난수에서 뽑는다고 적고, 그 대가까지 명시한다 — "A time-ordered identifier would be **the better database key**, and is deliberately not used: it would let anyone holding one id infer when neighbouring files were created and enumerate towards them." 결정과 그 결정이 포기한 것을 함께 적는 서술이다.
|
||
|
||
경계 규칙도 실재한다. CLAUDE.md가 이름을 대는 ArchUnit 규칙은 `CleanArchitectureTest`에 **대문자 상수** `IDENTIFIER_ADAPTER_DOES_NOT_DEPEND_ON_OTHER_ADAPTERS_OR_BOOTSTRAP`로 존재하고, inbound web · persistence · bootstrap · Spring Data · JPA · Hibernate를 금지하며 형제 배제(`..adapter.outbound..` 중 자기 패키지 제외)까지 처리한다(`140-...` §8.4e). CLAUDE.md의 표기가 snake_case일 뿐 가드는 진짜다 — **confirmed match**.
|
||
|
||
## 3. P2 — 모듈의 존재 논거인 `UuidCodec`에 production 소비자가 없다
|
||
|
||
`140-...` §8.1·§8.1b의 저장소 전수 검색 결과:
|
||
|
||
| 타입 | leaf 밖 production 소비자 |
|
||
|---|---|
|
||
| `HmacUserPrincipalPseudonymizer` | `app-bootstrap`, `sample-portfolio` (각 1) |
|
||
| `RandomUploadIdentifierFactory` | `app-bootstrap` (1) |
|
||
| **`UuidCodec`** | **0** |
|
||
|
||
`UuidCodec.` 형태의 호출은 저장소 전체에서 **자기 Spock 스펙 5줄뿐**이다. 이름이 겹쳐 걸린 나머지 둘은 무관하다 — mongo testkit의 `org.bson.codecs.UuidCodec`(드라이버 타입)과 `sample-portfolio/README.md:333`의 산문 언급("`UuidCodec` 같은 공용…").
|
||
|
||
그 자리를 대신 채우고 있는 것들이 있다. `UUID.fromString`을 직접 부르는 파일이 leaf 밖에 20개 이상이고(graphql `UuidScalar`, notification `JacksonRoutingPlanCodec`, jpa `PostgreSqlIdempotencyClaimRepository`, application-core `FileId`/`UploadId` …), CLAUDE.md가 `toUuid`/`fromUuid`의 목적으로 든 **PostgreSQL `uuid` 컬럼 변환(D10)**은 실제로는 Hibernate의 `@JdbcTypeCode(SqlTypes.UUID)`가 처리한다(`140-...` §8.4b, JPA 엔티티 다수).
|
||
|
||
**판정: P2.** 코드 자체에는 결함이 없다 — 30줄짜리 유틸이고 자기 스펙을 통과한다. 문제는 §1의 논거다. 모듈을 `adapter-outbound` 밖에 두는 근거로 "UUID id/codec 능력"을 들고 있는데, 그 능력은 아무도 쓰지 않고 같은 일이 저장소 곳곳에서 각자 수행된다. 나머지 두 타입(가명화·업로드 식별자)만으로도 non-IO 능력 모듈의 논거는 성립하므로, 수정은 둘 중 하나다: `UuidCodec`을 실제 단일 경로로 만들거나(그러면 §4가 먼저 고쳐져야 한다), 모듈의 논거에서 빼는 것.
|
||
|
||
## 4. P2 — `normalize`는 canonical이 아닌 입력을 받아 다른 UUID로 조용히 바꾼다
|
||
|
||
`UuidCodec.normalize`의 계약은 Javadoc과 README 양쪽에 적혀 있다.
|
||
|
||
> Accepts a **case-insensitive canonical UUID** string and returns the canonical 36-character lowercase form… `@throws IllegalArgumentException` on a **malformed** UUID.
|
||
> (README) 형식 오류 UUID 에는 `IllegalArgumentException`.
|
||
|
||
구현은 `UUID.fromString(input).toString()` 한 줄이다. JDK의 `UUID.fromString`은 길이 36 fast path 밖에서 **대시로 나뉜 5개 hex 그룹을 길이 검사 없이** 받는다. 실행 probe로 확인했다(`140-...` EXECUTION PROBE).
|
||
|
||
```
|
||
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("not-a-uuid") -> IllegalArgumentException
|
||
```
|
||
|
||
`"1-1-1-1-1"`은 canonical UUID가 아니다. 계약대로면 `IllegalArgumentException`이어야 하는데, 수용된 뒤 **다른 문자열로 재작성되어** 반환된다. 결과적으로 서로 다른 두 입력(`"1-1-1-1-1"`과 `"00000001-0001-0001-0001-000000000001"`)이 같은 식별자로 정규화되고, 거부됐어야 할 값이 정상적으로 보이는 id가 된다. `normalize`는 D3 — 호출자가 준 텍스트를 저장 형태로 바꾸는 지점 — 이므로, 관대함이 남는 위치가 하필 신뢰 경계다.
|
||
|
||
기존 스펙이 이것을 놓친 이유도 코드에 있다. `UuidCodecSpec`의 거부 케이스는 `"not-a-uuid"` **하나**이고, 그 문자열은 대시 그룹이 5개가 아니라 관대한 경로에 닿지 않는다.
|
||
|
||
**도달성.** 지금 이 메서드를 부르는 production 코드는 없다(§3). 그래서 현재 노출은 0이고, `UuidCodec`을 단일 경로로 승격하는 순간 결함이 된다. **판정: P2.** 수정은 `input.length() != 36`이거나 대시 위치가 8-13-18-23이 아니면 먼저 거부하는 것 — 또는 계약 문구를 실제 동작(JDK 관대 파싱)에 맞추는 것이다. 전자가 문서가 말하는 바다.
|
||
|
||
## 5. P2 — 문서는 UUIDv7이라고 말하고, 생성되는 것은 v4다
|
||
|
||
CLAUDE.md:21과 README:19가 같은 문장을 쓴다 — `UuidCodec`은 "JDK `java.util.UUID` (**RFC 9562 UUIDv7**)" 위에서 동작한다. README:12도 이 모듈의 능력을 "식별자 생성/인코딩(**UUIDv7**)"이라 적는다. 그리고 이 주장은 leaf 밖으로도 번져 있다 — `CleanArchitectureTest`가 이 패키지를 설명하는 세 곳(:948, :981, :1310)이 전부 "UUIDv7 id/event-id generation & codec"이라고 쓴다.
|
||
|
||
실행 probe(`140-...`):
|
||
|
||
```
|
||
PROBE newFileId version=4 variant=2
|
||
PROBE newUploadId version=4 variant=2
|
||
PROBE UUID.randomUUID() version=4 (RFC 9562 UUIDv7 would report version=7)
|
||
```
|
||
|
||
`java.util.UUID`에는 v7 생성기가 없고, 이 leaf의 유일한 생성기 `RandomUploadIdentifierFactory`는 `UUID.randomUUID()`(v4)를 쓴다. 더 분명한 것은 **그 클래스의 javadoc이 v7을 명시적으로 거부한다**는 점이다 — "A time-ordered identifier would be the better database key, and is **deliberately not used**." 즉 코드는 숙고된 결정을 내렸고, 모듈 문서와 아키텍처 test의 설명문이 **정반대 결정을 서술**한다.
|
||
|
||
저장소의 진짜 UUIDv7은 다른 곳에 있다: `sample-portfolio`가 `com.github.f4b6a3:uuid-creator`를 자기 `build.gradle`에 선언하고 `UuidCreator.getTimeOrderedEpochPlus1()`을 쓴다(`SampleOperationStore:27`).
|
||
|
||
**판정: P2.** 런타임 동작은 옳다 — 잘못된 것은 세 문서와 한 test의 설명문이다. 그러나 "이 모듈이 UUIDv7을 제공한다"는 서술은 fork가 시간정렬 키를 기대하게 만들고, 그 기대는 인덱스 지역성과 열거 가능성 양쪽에서 반대 방향의 결과를 낳는다.
|
||
|
||
## 6. P3 — CLAUDE.md의 의존성 서술이 세 항목 모두 틀렸다
|
||
|
||
CLAUDE.md:31–33:
|
||
|
||
> `:application-code`, `:domain-core`, `:shared-contract` (Gradle matrix). **Currently only `:domain-core` + `com.github.f4b6a3:uuid-creator` are declared in build.gradle.**
|
||
|
||
`build.gradle`의 실제 `dependencies` 블록은 두 줄이다.
|
||
|
||
```groovy
|
||
implementation project(':application-core')
|
||
testImplementation 'org.spockframework:spock-core:2.4-groovy-5.0'
|
||
```
|
||
|
||
- `:domain-core` — 선언돼 있지 **않다**.
|
||
- `uuid-creator` — 선언돼 있지 **않다**(저장소 검색상 이 leaf의 classpath에 없다; `sample-portfolio`와 `app-bootstrap`의 lockfile에만 있다).
|
||
- `:application-core` — 선언돼 **있는데** 문장은 언급하지 않는다.
|
||
|
||
레지스트리의 `allowed_dependencies`(`domain-core`, `application-core`)와 실제 선언(부분집합)은 정합한다. 어긋난 것은 CLAUDE.md의 서술뿐이다. **P3.**
|
||
|
||
## 7. P3 — README의 세 가지 사실 오류
|
||
|
||
| README | 실제 |
|
||
|---|---|
|
||
| :3 패키지 루트 `dev.caskeleton.adapter.identifier` | `dev.caskeleton.adapter.outbound.identifier` (CLAUDE.md:11은 정확) |
|
||
| :59 "Spock 2.4 / **Groovy 4.0** variant" | `spock-core:2.4-groovy-**5.0**` |
|
||
| :64 edge는 `src/build.gradle`의 `allowedProjectDependencies['**adapter-identifier**']`로 허용 | `build.gradle:1416`의 `allowedProjectDependencies`는 리터럴 맵이 아니라 `registry.modules.collectEntries { … }`로 **레지스트리에서 파생**되며, 이 모듈의 키는 `adapter-outbound-identifier`다 |
|
||
|
||
셋 다 메커니즘 자체는 실재하고 동작한다 — 틀린 것은 이름과 버전이다. **P3.**
|
||
|
||
## 8. P3 — CLAUDE.md가 대는 두 가드 중 하나는 저장소에 없다
|
||
|
||
CLAUDE.md:38–39가 금지 사항의 근거로 둘을 든다.
|
||
|
||
1. ArchUnit `identifier_adapter_does_not_depend_on_other_adapters_or_bootstrap` — **존재한다**(§2, 상수명은 대문자). confirmed.
|
||
2. `.claude/hooks/ca_import_gate.py` **G4가 쓰기 시점에 차단** — `.claude/` 디렉터리에는 `settings.local.json` 하나뿐이고 `hooks/` 하위 디렉터리도 `ca_import_gate.py`도 **tracked 되어 있지 않다**(`140-...` §8.4e).
|
||
|
||
개발자 머신에 로컬로 존재할 여지는 있으나, 저장소를 새로 clone한 사람에게 그 가드는 없다. "쓰기 시점에 차단된다"는 서술은 clone에서 성립하지 않는다. **P3.**
|
||
|
||
## 9. P3/기록 — 결정 SSOT가 이 revision에서 해석되지 않는다
|
||
|
||
CLAUDE.md와 README가 `UuidCodec`의 동작을 **D3**(normalize)와 **D10**(toUuid/fromUuid)로 지목하고, 모듈 분류 근거로 "feature-resource-identifier-contract §4 taxonomy"를 든다. `CleanArchitectureTest`도 같은 문서를 §4·D5·D9·D17로 네 곳에서 인용한다.
|
||
|
||
그 문서는 이 revision에 **파일로 존재하지 않는다**(`find -iname '*resource-identifier*'` 매치 0; `docs/`에서 걸리는 D3/D10은 전부 MongoDB의 무관한 노출 평면 표기다). `CleanArchitectureTest:2432`의 주석이 이유를 밝힌다 — "decision SSOT: **resource-identifier branch**". 즉 다른 브랜치에 있다.
|
||
|
||
은폐가 아니라 명시된 상태이므로 결함으로 올리지 않고 기록한다. 다만 이 leaf의 문서가 자기 동작의 근거로 대는 결정 ID들은 이 브랜치만 읽어서는 확인할 수 없고, §4·§5의 어긋남이 "구현이 결정을 벗어난 것"인지 "결정이 그 사이 바뀐 것"인지도 여기서는 판정 불가다. **P3/기록.**
|
||
|
||
## 10. Negative-space probes
|
||
|
||
- **8.1 public-surface reachability**: 세 production 타입의 저장소 전수 소비자 계수. `UuidCodec` 0(§3), 나머지 둘은 composition root가 배선.
|
||
- **8.2 조건부 형제 비교**: `UuidCodec`의 세 자매 메서드가 null을 다르게 다룬다 — `normalize(null)` → `null`(문서화됨), `toUuid(null)` → `NullPointerException`, `fromUuid(null)` → `NullPointerException`(둘 다 미문서화). 실행 probe로 확인. 계약 문구는 `normalize`에만 있다. **P3.**
|
||
- **8.2b 계약 ↔ 구현 대조**: `normalize`의 "canonical만 수용" 주장과 JDK 관대 파싱(§4). 실행 probe로 확정.
|
||
- **8.3 중복 mechanism sweep**: UUID 문자열 변환이 leaf 밖 20+ 파일에서 `UUID.fromString`으로 각자 수행되고, D10이 지목한 PostgreSQL `uuid` 컬럼 변환은 Hibernate `@JdbcTypeCode(SqlTypes.UUID)`가 담당(§3). 저장소의 UUIDv7 생성은 `sample-portfolio`의 `UuidCreator`(§5).
|
||
- **8.4 문서/개수 drift**: §5(UUID 버전, leaf 문서 3곳 + 아키텍처 test 3곳) · §6(의존성 서술 3항목) · §7(패키지 루트·Groovy 버전·설정 키) · §8(가드 하나 부재) · §9(결정 SSOT 미해석).
|
||
|
||
## 11. Findings backlog
|
||
|
||
| 우선순위 | finding | reachability |
|
||
|---|---|---|
|
||
| **P2** | `UuidCodec.normalize`가 canonical이 아닌 5-그룹 입력(`"1-1-1-1-1"` 등)을 수용해 다른 UUID로 재작성한다 — Javadoc·README는 canonical만 받고 malformed는 거부한다고 적는다 (실행 probe) | 현재 호출자 0; `UuidCodec`을 단일 경로로 쓰는 순간 신뢰 경계 결함 |
|
||
| **P2** | `UuidCodec`에 production 소비자가 0인데, 모듈을 `adapter-outbound` 밖에 두는 논거가 바로 이 "UUID id/codec 능력"이다. 같은 변환이 leaf 밖 20+ 파일에서 각자 수행되고 D10의 대상은 Hibernate가 처리한다 | 문서/모듈 경계 논거 |
|
||
| **P2** | leaf 문서 3곳과 `CleanArchitectureTest` 설명문 3곳이 "RFC 9562 UUIDv7"이라 적지만 유일한 생성기는 v4를 만들고, 그 클래스의 javadoc은 시간정렬 id를 명시적으로 거부한다 (실행 probe: version=4) | fork가 시간정렬 키를 기대하는 경우 |
|
||
| **P3** | CLAUDE.md:31–33의 build.gradle 선언 서술이 세 항목 모두 사실과 다르다 | 문서 |
|
||
| **P3** | README의 패키지 루트·Spock/Groovy variant·`allowedProjectDependencies` 키 3건 오류 | 문서 |
|
||
| **P3** | CLAUDE.md가 대는 write-time 가드 `.claude/hooks/ca_import_gate.py`가 저장소에 tracked 되어 있지 않다 | 새 clone |
|
||
| **P3** | `UuidCodec` 세 자매 메서드의 null 처리 비대칭이 `normalize`에만 문서화돼 있다 | 호출 시점 |
|
||
| **P3/기록** | 결정 SSOT `feature-resource-identifier-contract`(§4·D3·D5·D9·D10·D17)가 다른 브랜치에 있어 이 revision에서 해석되지 않는다 | 결정 대조 |
|
||
|
||
## 12. 완료 조건
|
||
|
||
- denominator **10 / 10 FULL_READ** — structural-only 0 · excluded 0 · unclassified 0 (§0)
|
||
- §8.1(공개 표면 도달성) · §8.2(조건부 형제) · §8.3(중복 mechanism) · §8.4(문서/개수 drift) 네 종 probe 수행
|
||
- 정적으로 결정 불가한 세 지점(normalize의 실제 수용 범위, 세 자매의 null 처리, 생성되는 UUID 버전)을 실행 probe로 확정(`140-...`)
|
||
- CLAUDE.md가 대는 두 가드를 각각 추적해 **하나는 실재(confirmed)**, 하나는 부재로 분리 판정 — 이름 표기 차이를 결함으로 올리지 않았다
|
||
- 임시 probe class 1개 추가 후 제거, `git status --short` = 0, 소스 미변경
|
||
|
||
## Source anchors
|
||
|
||
이 문서가 backtick으로 인용한 타입·경로를 저장소 트리에 대고 해석한 결과다. 해석된 것만 싣는다 — 총 **8개** (main 3 · test 1 · 기타 4).
|
||
|
||
```
|
||
src/adapter/outbound/identifier/build.gradle
|
||
src/config/architecture/modules.json (adapter-outbound-identifier 항목)
|
||
|
||
main:
|
||
src/main/java/dev/caskeleton/adapter/outbound/identifier/HmacUserPrincipalPseudonymizer.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/identifier/RandomUploadIdentifierFactory.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/identifier/UuidCodec.java
|
||
|
||
test:
|
||
src/test/java/dev/caskeleton/adapter/outbound/identifier/HmacUserPrincipalPseudonymizerTest.java
|
||
|
||
기타:
|
||
CLAUDE.md
|
||
README.md
|
||
src/build.gradle
|
||
src/sample-portfolio/README.md
|
||
|
||
해석되지 않은 인용 (3종) — 외부 타입·문서상 약칭 등:
|
||
evidence/raw/139-identifier-module-inventory.txt
|
||
evidence/raw/140-identifier-negative-space-probes.txt
|
||
settings.local.json
|
||
|
||
```
|