# 저장소 전체 (62 줄)
  GrpcOutcomeReplay.java:1 package dev.caskeleton.grpc.idempotency;
  GrpcOutcomeReplay.java:2 
  GrpcOutcomeReplay.java:3 import java.util.Optional;
  GrpcOutcomeReplay.java:4 import java.util.concurrent.ConcurrentHashMap;
  GrpcOutcomeReplay.java:5 import java.util.concurrent.ConcurrentMap;
  GrpcOutcomeReplay.java:6 
  GrpcOutcomeReplay.java:7 /**
  GrpcOutcomeReplay.java:8  * Returns the stored answer for an operation that already committed.
  GrpcOutcomeReplay.java:9  *
  GrpcOutcomeReplay.java:10  * <p>Separated from the ledger because the two have different size problems. The ledger row must
  GrpcOutcomeReplay.java:11  * stay small and is written inside the business transaction; a response may be large and is only
  GrpcOutcomeReplay.java:12  * needed if somebody actually retries. So the ledger stores a reference and this resolves it,
  GrpcOutcomeReplay.java:13  * against whatever the deployment chose — a small inline store, an object store, or a re-read of
  GrpcOutcomeReplay.java:14  * the committed resource.
  GrpcOutcomeReplay.java:15  */
  GrpcOutcomeReplay.java:16 public final class GrpcOutcomeReplay {
  GrpcOutcomeReplay.java:17 
  GrpcOutcomeReplay.java:18   private final ConcurrentMap<String, byte[]> storedOutcomes = new ConcurrentHashMap<>();
  GrpcOutcomeReplay.java:19   private final int maxInlineBytes;
  GrpcOutcomeReplay.java:20 
  GrpcOutcomeReplay.java:21   /**
  GrpcOutcomeReplay.java:22    * @param maxInlineBytes the largest response stored inline; anything larger must be kept behind a
  GrpcOutcomeReplay.java:23    *     reference in an object store instead, which is why {@link #store} refuses it rather than
  GrpcOutcomeReplay.java:24    *     silently truncating
  GrpcOutcomeReplay.java:25    */
  GrpcOutcomeReplay.java:26   public GrpcOutcomeReplay(int maxInlineBytes) {
  GrpcOutcomeReplay.java:27     if (maxInlineBytes < 1) {
  GrpcOutcomeReplay.java:28       throw new IllegalArgumentException("an inline outcome store needs a positive size limit");
  GrpcOutcomeReplay.java:29     }
  GrpcOutcomeReplay.java:30     this.maxInlineBytes = maxInlineBytes;
  GrpcOutcomeReplay.java:31   }
  GrpcOutcomeReplay.java:32 
  GrpcOutcomeReplay.java:33   /** Stores a response under {@code outcomeReference}. */
  GrpcOutcomeReplay.java:34   public void store(String outcomeReference, byte[] serializedResponse) {
  GrpcOutcomeReplay.java:35     if (outcomeReference == null || outcomeReference.isBlank()) {
  GrpcOutcomeReplay.java:36       throw new IllegalArgumentException("an outcome needs a reference to be stored under");
  GrpcOutcomeReplay.java:37     }
  GrpcOutcomeReplay.java:38     if (serializedResponse == null) {
  GrpcOutcomeReplay.java:39       throw new IllegalArgumentException("an outcome needs a serialized response");
  GrpcOutcomeReplay.java:40     }
  GrpcOutcomeReplay.java:41     if (serializedResponse.length > maxInlineBytes) {
  GrpcOutcomeReplay.java:42       throw new IllegalArgumentException(
  GrpcOutcomeReplay.java:43           "response of "
  GrpcOutcomeReplay.java:44               + serializedResponse.length
  GrpcOutcomeReplay.java:45               + " bytes exceeds the inline outcome limit of "
  GrpcOutcomeReplay.java:46               + maxInlineBytes
  GrpcOutcomeReplay.java:47               + "; store it behind an object reference instead");
  GrpcOutcomeReplay.java:48     }
  GrpcOutcomeReplay.java:49     storedOutcomes.put(outcomeReference, serializedResponse.clone());
  GrpcOutcomeReplay.java:50   }
  GrpcOutcomeReplay.java:51 
  GrpcOutcomeReplay.java:52   /** The stored response, if there is one. */
  GrpcOutcomeReplay.java:53   public Optional<byte[]> replay(String outcomeReference) {
  GrpcOutcomeReplay.java:54     byte[] stored = storedOutcomes.get(outcomeReference);
  GrpcOutcomeReplay.java:55     return Optional.ofNullable(stored).map(byte[]::clone);
  GrpcOutcomeReplay.java:56   }
  GrpcOutcomeReplay.java:57 
  GrpcOutcomeReplay.java:58   /** How many outcomes are held. */
  GrpcOutcomeReplay.java:59   public int size() {
  GrpcOutcomeReplay.java:60     return storedOutcomes.size();
  GrpcOutcomeReplay.java:61   }
  GrpcOutcomeReplay.java:62 }

# 그 맵에 가해지는 연산 전부
    GrpcOutcomeReplay.java:18  private final ConcurrentMap<String, byte[]> storedOutcomes = new ConcurrentHashMap<>();
    GrpcOutcomeReplay.java:49  storedOutcomes.put(outcomeReference, serializedResponse.clone());
    GrpcOutcomeReplay.java:54  byte[] stored = storedOutcomes.get(outcomeReference);
    GrpcOutcomeReplay.java:60  return storedOutcomes.size();
    클래스와 필드의 접근 한정 :
    GrpcOutcomeReplay.java:16  public final class GrpcOutcomeReplay {
    GrpcOutcomeReplay.java:18  private final ConcurrentMap<String, byte[]> storedOutcomes = new ConcurrentHashMap<>();
    줄이는 연산 이름 열여덟을 통틀어 : 0 건
    같은 검색을 대조 파일에 걸면    : 5 건  (검색이 헛돌지 않음을 보인다)

# 크기 상한이 실제로 거부하는가
    16 바이트 저장 후 size : 1
    17 바이트 저장 : response of 17 bytes exceeds the inline outcome limit of 16; store it behind an object reference instead
    거부 후 size            : 1
    서로 다른 키 1000 개 저장 후 size : 1001
    그 1000 개를 모두 replay 한 뒤    : 1001
    같은 키로 1000 번 덮어쓴 뒤      : 1001

# 이 타입이 나오는 파일 전부 — 경로 제한 없이, 절단 없이
  docs/2026-08-13-grpc-type-safe-rpc-platform-implementation-plan.md
  src/grpc/grpc-policy/src/main/java/dev/caskeleton/grpc/idempotency/GrpcOutcomeReplay.java
  src/grpc/grpc-policy/src/test/java/dev/caskeleton/grpc/idempotency/GrpcIdempotencyInterceptorTest.java
  자기 파일 밖 main 참조 : 0 개
  [대조] 같은 패키지의 GrpcIdempotencyDecision : 1 개  (0 이 아니어야 정상)
  git 이 추적하지 않는 변경 : 0 건
  .java 밖에서 이 이름을 쓰는 파일 : 1 개

# 같은 패키지의 인터셉터는 재생을 어떻게 판정하는가
  GrpcIdempotencyInterceptor.java:95     if (!record.sameRequestAs(requestFingerprint)) {
  GrpcIdempotencyInterceptor.java:96       return GrpcIdempotencyDecision.fingerprintMismatch();
  GrpcIdempotencyInterceptor.java:97     }
  GrpcIdempotencyInterceptor.java:98     if (record.state() == GrpcOperationLedgerState.COMMITTED) {
  GrpcIdempotencyInterceptor.java:99       return GrpcIdempotencyDecision.replay(
  GrpcIdempotencyInterceptor.java:100           record
  GrpcIdempotencyInterceptor.java:101               .outcomeReference()
  GrpcIdempotencyInterceptor.java:102               .orElseThrow(
  GrpcIdempotencyInterceptor.java:103                   () ->
  GrpcIdempotencyInterceptor.java:104                       new IllegalStateException(
  GrpcIdempotencyInterceptor.java:105                           "COMMITTED ledger record for '"
  GrpcIdempotencyInterceptor.java:106                               + identity.storageKey()
  GrpcIdempotencyInterceptor.java:107                               + "' has no outcome reference to replay")));
  GrpcIdempotencyInterceptor.java:108     }
  GrpcIdempotencyInterceptor.java:109     if (record.state() == GrpcOperationLedgerState.FAILED_TERMINAL) {
  GrpcIdempotencyInterceptor.java:110       // A terminal failure is not replayed: the caller asked for the operation, it did not happen,
  그 파일이 GrpcOutcomeReplay 를 부르는가 : 0 건

# 자바독이 이 타입을 무엇이라고 적는가
  GrpcOutcomeReplay.java:7 /**
  GrpcOutcomeReplay.java:8  * Returns the stored answer for an operation that already committed.
  GrpcOutcomeReplay.java:9  *
  GrpcOutcomeReplay.java:10  * <p>Separated from the ledger because the two have different size problems. The ledger row must
  GrpcOutcomeReplay.java:11  * stay small and is written inside the business transaction; a response may be large and is only
  GrpcOutcomeReplay.java:12  * needed if somebody actually retries. So the ledger stores a reference and this resolves it,
  GrpcOutcomeReplay.java:13  * against whatever the deployment chose — a small inline store, an object store, or a re-read of
  GrpcOutcomeReplay.java:14  * the committed resource.
  GrpcOutcomeReplay.java:15  */
  GrpcOutcomeReplay.java:16 public final class GrpcOutcomeReplay {

# 대조 클래스에 같은 잣대를 댄다 (123 줄)
  이 파일   : src/grpc/grpc-policy/src/main/java/dev/caskeleton/grpc/idempotency/GrpcOutcomeReplay.java
  대조 파일 : src/grpc-advanced/grpc-advanced-streaming/src/main/java/dev/caskeleton/grpc/advanced/streaming/GrpcClientMessageDeduplicator.java
  GrpcClientMessageDeduplicator.java:11  * <p>Checkpoint-based rather than a set of seen keys. A set grows without bound for the life of a
  GrpcClientMessageDeduplicator.java:12  * session and answers "have I seen this" — which is not quite the question. The question is "has
  GrpcClientMessageDeduplicator.java:13  * this been applied", and a monotonic applied-sequence answers it in constant space and survives
  GrpcClientMessageDeduplicator.java:14  * the process restart that a set does not.
  GrpcClientMessageDeduplicator.java:16  * <p>Replayed outcomes are kept for the small window after the checkpoint, so a duplicate that
  GrpcClientMessageDeduplicator.java:17  * arrives before the checkpoint advances gets the original answer rather than being reapplied.
  GrpcClientMessageDeduplicator.java:18  */
  GrpcClientMessageDeduplicator.java:19 public final class GrpcClientMessageDeduplicator {
  GrpcClientMessageDeduplicator.java:20 
  GrpcClientMessageDeduplicator.java:21   private final ConcurrentMap<String, GrpcClientStreamCheckpoint> checkpoints =
  GrpcClientMessageDeduplicator.java:22       new ConcurrentHashMap<>();
  GrpcClientMessageDeduplicator.java:23   private final ConcurrentMap<String, String> replayableOutcomes = new ConcurrentHashMap<>();
  적용된 메시지마다 무엇을 넣는가 :
  GrpcClientMessageDeduplicator.java:70   public void recordApplied(
  GrpcClientMessageDeduplicator.java:71       GrpcClientStreamMessage<?> message, String outcomeReference, Instant at) {
  GrpcClientMessageDeduplicator.java:72     GrpcClientStreamCheckpoint checkpoint = requireCheckpoint(message.sessionId());
  GrpcClientMessageDeduplicator.java:73     checkpoints.put(message.sessionId().value(), checkpoint.advancedTo(message.sequence(), at));
  GrpcClientMessageDeduplicator.java:74     if (outcomeReference != null && !outcomeReference.isBlank()) {
  GrpcClientMessageDeduplicator.java:75       replayableOutcomes.put(message.dedupKey(), outcomeReference);
  GrpcClientMessageDeduplicator.java:76     }
  GrpcClientMessageDeduplicator.java:77   }
  정리 경로와 그 호출자 :
    main · GrpcClientMessageDeduplicator.java:110   public void endSession(GrpcClientStreamSessionId sessionId) {
    test · GrpcClientMessageDeduplicatorTest.java:117     deduplicator.endSession(session);
    그 대조 클래스의 자기 파일 밖 main 참조 : 0 개

# 두 클래스가 같은 family 인가
  modules.json 의 family 키 : 0 건
  src/grpc/CLAUDE.md:1 # grpc — local authority for the gRPC platform family
  src/grpc/CLAUDE.md:2 
  src/grpc/CLAUDE.md:3 이 문서는 `grpc:*` family의 **local authority**다. leaf 목록·gradle path·허용 의존성은
  src/grpc-advanced/CLAUDE.md:3 이 문서는 `grpc-advanced:*` family의 **local authority**다. leaf 목록·gradle path·허용 의존성은
  src/grpc-advanced/CLAUDE.md:4 `src/config/architecture/modules.json`이 SSOT다. Root 정책(`CLAUDE.md` / `AGENTS.md`)과 충돌하면
  src/grpc-advanced/CLAUDE.md:5 root가 이긴다.
  src/grpc-advanced/CLAUDE.md:6 
  src/grpc-advanced/CLAUDE.md:7 이 family는 Stable gRPC 플랫폼(`grpc:*`)이 **의도적으로 제외한** 능력들을 담는다. 별도 디렉터리와
  src/grpc-advanced/CLAUDE.md:8 별도 Gradle prefix인 이유는 하나다: "Stable starter가 advanced module을 참조하면 build가 실패한다"는
  src/grpc-advanced/CLAUDE.md:9 불변 조건을 registry의 `allowed_dependencies`만으로 기계 검증할 수 있게 하기 위해서다.
  src/grpc-advanced/CLAUDE.md:15  grpc:*          → grpc-advanced:*  (금지 — registry가 거부한다)
  src/grpc-advanced/CLAUDE.md:74  - Stable leaf(`grpc:*`)가 이 family를 참조하는 것.
