adapter-outbound-cache-redis — 모듈 인벤토리
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
generatedAt=2026-08-30T03:57:11+00:00

=== tracked file 총계 ===
$ git ls-files 'adapter/outbound/cache-redis' | wc -l
390
exit=0

$ git ls-files 'adapter/outbound/cache-redis' | sed 's|^adapter/outbound/cache-redis/||' | awk -F/ '{print $1"/"$2}' | sort | uniq -c | sort -rn
    316 src/main
     70 src/test
      1 gradle.lockfile/
      1 build.gradle/
      1 README.md/
      1 CLAUDE.md/
exit=0

=== source set 별 ===
main: 316 files, 33489 LOC
test: 70 files, 15244 LOC

=== main 패키지 배치 ===
$ git ls-files 'adapter/outbound/cache-redis/src/main' | grep '\.java$' | sed 's|.*/dev/caskeleton/||' | xargs -n1 dirname | sort | uniq -c | sort -rn
     69 adapter/outbound/cache/redis/sdk/api/operations
     67 adapter/outbound/cache/redis/sdk/lettuce/operations
     17 adapter/outbound/cache/redis/sdk/programmability
     17 adapter/outbound/cache/redis/sdk/api/reactive
     17 adapter/outbound/cache/redis/sdk/api/key
     14 adapter/outbound/cache/redis/sdk/lettuce/command
     14 adapter/outbound/cache/redis/sdk/api/error
     13 adapter/outbound/cache/redis/sdk/api/command
     12 adapter/outbound/cache/redis/sdk/config
      9 adapter/outbound/cache/redis/sdk/lettuce/connection
      7 adapter/outbound/cache/redis/sdk/raw
      7 adapter/outbound/cache/redis/sdk/lettuce/codec
      7 adapter/outbound/cache/redis/sdk/extensions/search
      6 adapter/outbound/cache/redis/sdk/api
      5 adapter/outbound/cache/redis/sdk/cluster
      4 adapter/outbound/cache/redis/sdk/extensions/timeseries
      4 adapter/outbound/cache/redis/sdk/admin
      4 adapter/outbound/cache/redis/realtime
      3 adapter/outbound/cache/redis/sdk/extensions/json
      3 adapter/outbound/cache/redis/sdk/api/codec
      3 adapter/outbound/cache/redis/ratelimit
      2 adapter/outbound/cache/redis/sdk/lettuce/observability
      2 adapter/outbound/cache/redis/sdk/extensions/probabilistic
      2 adapter/outbound/cache/redis/lease
      2 adapter/outbound/cache/redis/idempotency
      2 adapter/outbound/cache/redis/cache
      1 adapter/outbound/cache/redis/sdk/extensions
      1 adapter/outbound/cache/redis/keyspace
exit=0

=== main 상위 30 파일 ===
$ git ls-files 'adapter/outbound/cache-redis/src/main' | grep '\.java$' | xargs wc -l | sort -rn | head -31
  32082 total
   1282 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/LettuceRedisCommandGateway.java
   1227 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/RedisCommandGateway.java
    959 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisSdkSettings.java
    600 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/connection/RedisTopologyClientFactory.java
    529 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/idempotency/RedisIdempotencyStoreAdapter.java
    487 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/StreamOperationRequests.java
    428 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/cache/RedisCacheRegionAdapter.java
    422 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/HashOperationRequests.java
    413 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/SortedSetOperationRequests.java
    405 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/RedisOperationContext.java
    403 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/lease/RedisDistributedLeaseAdapter.java
    397 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisSdkAutoConfiguration.java
    361 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/programmability/LettuceRedisTransactionOperations.java
    344 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/ListOperationRequests.java
    337 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/admin/LettuceRedisAdminOperations.java
    333 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/SetOperationRequests.java
    320 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/ValueOperationRequests.java
    316 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/connection/RedisRuntimeOwner.java
    306 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/KeyOperationRequests.java
    306 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/idempotency/IdempotencyScripts.java
    301 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/realtime/RedisConnectionRegistryAdapter.java
    280 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/ratelimit/RateLimitScripts.java
    278 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/CommandPolicyGuard.java
    268 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/BatchExecution.java
    268 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/RedisCommandPolicyLoader.java
    251 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/GeoOperationRequests.java
    247 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/LettuceRedisBatch.java
    234 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/LettuceExceptionTranslator.java
    230 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/extensions/timeseries/LettuceRedisTimeSeriesOperations.java
    220 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/extensions/probabilistic/LettuceRedisProbabilisticOperations.java
exit=0

=== governance / 루트 파일 ===
$ git ls-files 'adapter/outbound/cache-redis' | grep -v '^adapter/outbound/cache-redis/src/'
adapter/outbound/cache-redis/CLAUDE.md
adapter/outbound/cache-redis/README.md
adapter/outbound/cache-redis/build.gradle
adapter/outbound/cache-redis/gradle.lockfile
exit=0

=== 레지스트리 엔트리 ===
$ python3 -c "
import json;d=json.load(open('config/architecture/modules.json'))
for m in d['modules']:
    if m['id']=='adapter-outbound-cache-redis': print(json.dumps(m,ensure_ascii=False,indent=2))
"
{
  "id": "adapter-outbound-cache-redis",
  "gradle_path": ":adapter:outbound:cache-redis",
  "source_path": "src/adapter/outbound/cache-redis",
  "allowed_dependencies": [
    "domain-core",
    "application-core",
    "shared-contract",
    "adapter-outbound-support"
  ],
  "runtime_memberships": [
    "app-bootstrap"
  ]
}
exit=0

=== build.gradle ===
$ cat -n 'adapter/outbound/cache-redis/build.gradle'
     1	// Redis SDK leaf — see docs/superpowers/specs/2026-08-07-redis-wrapper-typed-api-design.md.
     2	//
     3	// The design models the SDK as separate Gradle modules. This repository's fail-closed module
     4	// registry outranks that layout, so the module boundaries are packages under
     5	// dev.caskeleton.adapter.outbound.cache.redis.sdk and RedisSdkModuleBoundaryTest enforces them.
     6	dependencies {
     7	    // Registered edges the semantic port adapters need. The SDK itself imports nothing from them
     8	    // today (0 imports across main source) — the semantic cache/session/idempotency/rate-limit
     9	    // adapters that did were removed and are restored by Phase E of
    10	    // docs/superpowers/plans/2026-08-10-redis-optionality-and-composition.md. They stay declared
    11	    // because that restoration is the module's stated responsibility, not because anything here
    12	    // compiles against them.
    13	    implementation project(':application-core')
    14	    implementation project(':shared-contract')
    15	    implementation project(':adapter:outbound:support')
    16	
    17	    implementation 'org.springframework.boot:spring-boot-autoconfigure'
    18	    // The role-aware health contributors are HealthIndicators; the readiness probe is the only
    19	    // place the required/optional Redis taxonomy can actually be enforced.
    20	    implementation 'org.springframework.boot:spring-boot-health'
    21	    // Boot's Health type carries Jackson annotations. Without the annotations on the compile
    22	    // classpath javac emits an 'unknown enum constant' warning, and this build is -Werror. Runtime
    23	    // does not need it from here — the app already has Jackson — so compileOnly is the honest scope.
    24	    compileOnly 'com.fasterxml.jackson.core:jackson-annotations'
    25	    implementation 'io.lettuce:lettuce-core'
    26	    // Reactor is in the public signature of sdk.api.reactive, and it was reaching this module only
    27	    // transitively through lettuce-core. A driver upgrade that stopped exposing it would have
    28	    // broken compilation of the SDK's own published API, so it is declared directly.
    29	    implementation 'io.projectreactor:reactor-core'
    30	    implementation 'org.slf4j:slf4j-api'
    31	    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    32	
    33	    // Deliberately absent:
    34	    //   org.springframework.data:spring-data-redis — the SDK owns its own typed API and command
    35	    //     policy on purpose; routing through Spring Data would reintroduce the untyped, unguarded
    36	    //     command surface the catalog exists to prevent. Zero imports.
    37	    //   io.micrometer:micrometer-core — observation leaves this leaf as RedisObservation through a
    38	    //     Consumer sink; binding it to a meter registry belongs to the composition root, not here.
    39	    //     Zero imports.
    40	}
    41	
    42	tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' }
    43	
    44	// The topology lane is opt-in and fail-closed. The default unit task excludes it, and selecting it
    45	// without an endpoint is an error rather than a skip: a topology test that silently passes because
    46	// it never connected is worse than not having one.
    47	tasks.named('test') {
    48	    useJUnitPlatform {
    49	        excludeTags 'redis-topology'
    50	    }
    51	}
    52	
    53	// Lane selection is derived from the declared mode rather than chosen by hand. A promotion test is
    54	// meaningless without sentinels and a cross-slot test is meaningless without a cluster, but writing
    55	// that as a runtime assumption would turn "the lane was never started" into a green skip. Selecting
    56	// by tag keeps the lane fail-closed: what a mode cannot prove is not selected, and what is selected
    57	// must pass.
    58	//
    59	// The mode is an allowlist, not free text. Deriving the tag from an arbitrary property produced the
    60	// worst possible result for a qualification lane: `-Predis.topology.mode=TYPO` built the tag
    61	// `lane-typo`, matched nothing, ran zero tests and exited 0. A release gate that reports success
    62	// for a lane it never ran is worse than no gate, so an unknown mode is an error and a run that
    63	// executed no test is a failure.
    64	// `tls` is a lane, not a deployment mode. Its shape is standalone; what it qualifies is the
    65	// transport, which no other lane carries a single command over. It was reachable only by hand —
    66	// point LiveRedisCompositionTest at the TLS compose with an ad-hoc init script — which is another
    67	// way of saying the release gate did not cover TLS at all.
    68	def REDIS_TOPOLOGY_MODES = ['standalone', 'sentinel', 'cluster', 'tls'] as Set
    69	def REDIS_TOPOLOGY_DEPLOYMENT_MODE = ['standalone': 'standalone', 'sentinel': 'sentinel',
    70	                                      'cluster': 'cluster', 'tls': 'standalone']
    71	// The classes each lane exists to run, and the floor below which its coverage has shrunk. Both are
    72	// declarations rather than observations: a lane that lost a class to a rename, or lost half its
    73	// cases to a filter, otherwise still reports success.
    74	def REDIS_TOPOLOGY_REQUIRED_CLASSES = [
    75	        'standalone': ['LiveRedisCompositionTest', 'LiveRedisSemanticPortsTest',
    76	                       'RedisTopologyContractTest', 'LiveRedisGuardrailTest'],
    77	        'sentinel'  : ['LiveRedisCompositionTest', 'LiveRedisSentinelPromotionTest',
    78	                       'RedisTopologyContractTest'],
    79	        'cluster'   : ['LiveRedisCompositionTest', 'LiveRedisClusterTest',
    80	                       'LiveRedisClusterTransactionTest', 'LiveRedisSemanticPortsTest'],
    81	        'tls'       : ['LiveRedisTlsTest'],
    82	]
    83	def REDIS_TOPOLOGY_MINIMUM_TESTS = ['standalone': 20, 'sentinel': 20, 'cluster': 24, 'tls': 4]
    84	
    85	tasks.register('redisTopologyTest', Test) {
    86	    description = 'Runs the Redis SDK contracts against a real topology declared in infra/redis-sdk.'
    87	    group = 'verification'
    88	    testClassesDirs = sourceSets.test.output.classesDirs
    89	    classpath = sourceSets.test.runtimeClasspath
    90	    // Never up to date. This task's result depends on a server outside the build, so Gradle's
    91	    // inputs say nothing about whether it would still pass: re-running it against a lane that was
    92	    // restarted, reconfigured, or promoted reports the previous run's verdict as the current one.
    93	    // That is the same silent-pass failure mode the fail-closed endpoint check exists to prevent.
    94	    outputs.upToDateWhen { false }
    95	    def declaredMode = (project.findProperty('redis.topology.mode') ?: 'unset').toString().toLowerCase()
    96	    useJUnitPlatform {
    97	        includeTags "redis-topology & lane-${declaredMode}".toString()
    98	    }
    99	    // A filter that matches nothing is a configuration mistake, never a pass.
   100	    failOnNoDiscoveredTests = true
   101	    ['redis.topology.host', 'redis.topology.port',
   102	     'redis.topology.master', 'redis.topology.username', 'redis.topology.password',
   103	     'redis.topology.trust-material']
   104	            .each { key ->
   105	                if (project.hasProperty(key)) {
   106	                    systemProperty key, project.property(key)
   107	                }
   108	            }
   109	    // The lane name and the deployment mode are different things, and only the TLS lane makes that
   110	    // visible: its shape is standalone, so the tests must see `standalone` while the tag filter and
   111	    // the required properties come from the lane. Passing the lane name through as the mode would
   112	    // fail RedisDeploymentMode.valueOf on a value that is not a topology.
   113	    systemProperty 'redis.topology.mode', REDIS_TOPOLOGY_DEPLOYMENT_MODE.getOrDefault(declaredMode, declaredMode)
   114	    systemProperty 'redis.topology.tls', (declaredMode == 'tls').toString()
   115	
   116	    // Executed, not merely reported. `afterTest` fires for a skipped test too, so counting every
   117	    // callback meant a lane whose tests all skipped could still satisfy the "ran something" check —
   118	    // the exact green-for-nothing this gate exists to prevent, one level further in.
   119	    def executed = new java.util.concurrent.atomic.AtomicInteger()
   120	    def skipped = new java.util.concurrent.atomic.AtomicInteger()
   121	    def classes = java.util.Collections.synchronizedSet(new java.util.LinkedHashSet<String>())
   122	    afterTest { descriptor, result ->
   123	        if (result.resultType == org.gradle.api.tasks.testing.TestResult.ResultType.SKIPPED) {
   124	            skipped.incrementAndGet()
   125	        } else {
   126	            executed.incrementAndGet()
   127	            classes.add(descriptor.className.tokenize('.').last())
   128	        }
   129	    }
   130	
   131	    doFirst {
   132	        if (!REDIS_TOPOLOGY_MODES.contains(declaredMode)) {
   133	            throw new GradleException(
   134	                    "redisTopologyTest was selected with redis.topology.mode='${declaredMode}'; " +
   135	                    'the supported modes are ' + REDIS_TOPOLOGY_MODES.sort().join(', ') +
   136	                    '. An unrecognised mode selects no test and would otherwise report success.')
   137	        }
   138	        def required = ['redis.topology.host', 'redis.topology.port']
   139	        if (declaredMode == 'sentinel') {
   140	            required += 'redis.topology.master'
   141	        }
   142	        if (declaredMode == 'tls') {
   143	            // Without the trust material the client would have to disable verification to connect,
   144	            // and a TLS lane that trusts anything qualifies nothing.
   145	            required += 'redis.topology.trust-material'
   146	        }
   147	        def missing = required.findAll { !project.hasProperty(it) }
   148	        if (!missing.isEmpty()) {
   149	            throw new GradleException(
   150	                    'redisTopologyTest was selected without ' + missing.join(', ') +
   151	                    '; start a lane from infra/redis-sdk and pass -P<key>=<value>.')
   152	        }
   153	        // The lane's tag must actually exist in the compiled suite. failOnNoDiscoveredTests catches
   154	        // an empty run, but this names the cause — a renamed or deleted lane class — instead of
   155	        // leaving an operator to guess whether the filter or the server is at fault.
   156	        def laneTag = "lane-${declaredMode}"
   157	        def tagged = sourceSets.test.allJava.matching { include '**/*.java' }.files.any { file ->
   158	            def text = file.text
   159	            text.contains('@Tag("redis-topology")') && text.contains("@Tag(\"${laneTag}\")")
   160	        }
   161	        if (!tagged) {
   162	            throw new GradleException(
   163	                    "redisTopologyTest found no test class tagged 'redis-topology' and " +
   164	                    "'${laneTag}'. The ${declaredMode} lane has no coverage to run, so a green " +
   165	                    'result would prove nothing.')
   166	        }
   167	    }
   168	
   169	    doLast {
   170	        if (executed.get() < 1) {
   171	            throw new GradleException(
   172	                    "redisTopologyTest completed without executing a single test for the " +
   173	                    "${declaredMode} lane. A qualification lane that runs nothing must not report " +
   174	                    'success.')
   175	        }
   176	        // What a lane must cover, named rather than counted by accident. A tag filter matching one
   177	        // trivial class satisfied "ran something" while the class the lane exists for had been
   178	        // renamed out of the filter, and nothing said so.
   179	        def required = REDIS_TOPOLOGY_REQUIRED_CLASSES[declaredMode]
   180	        def absent = required.findAll { !classes.contains(it) }
   181	        if (!absent.isEmpty()) {
   182	            throw new GradleException(
   183	                    "redisTopologyTest ran the ${declaredMode} lane without ${absent.join(', ')}. " +
   184	                    'These classes are what the lane qualifies; a run that skipped them proves ' +
   185	                    'less than the lane claims.')
   186	        }
   187	        def floor = REDIS_TOPOLOGY_MINIMUM_TESTS[declaredMode]
   188	        if (executed.get() < floor) {
   189	            throw new GradleException(
   190	                    "redisTopologyTest executed ${executed.get()} tests for the ${declaredMode} " +
   191	                    "lane, below the declared floor of ${floor}. Coverage that silently shrank is " +
   192	                    'a gate that silently weakened.')
   193	        }
   194	        if (skipped.get() > 0) {
   195	            throw new GradleException(
   196	                    "redisTopologyTest skipped ${skipped.get()} test(s) on the ${declaredMode} " +
   197	                    'lane. A qualification lane has no conditional coverage: what it cannot prove ' +
   198	                    'must not be selected, and what is selected must run.')
   199	        }
   200	        logger.lifecycle("redisTopologyTest: ${declaredMode} lane executed ${executed.get()} tests.")
   201	    }
   202	}
exit=0

=== leaf 밖 소비자 ===
$ git grep -l 'dev.caskeleton.adapter.outbound.cache' -- . | grep -v '^adapter/outbound/cache-redis/' | sed 's|/src/.*||' | sort | uniq -c
      7 app-bootstrap
exit=0

=== test resources ===
$ git ls-files 'adapter/outbound/cache-redis/src' | grep -v '\.java$' | grep -v '\.groovy$'
adapter/outbound/cache-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
adapter/outbound/cache-redis/src/main/resources/redis-sdk/redis-command-policy.yml
adapter/outbound/cache-redis/src/test/resources/redis-sdk/golden/order-summary-v1.json
exit=0

