70 KiB
title, source_type, status, branch, parent_branch, related_projects, governing_docs, tags, created, target_merge, status_label, id, kind, project, work_item, inherits, refines, overrides, depends_on, imports, delegates, accepts_delegations, contract_packet
| title | source_type | status | branch | parent_branch | related_projects | governing_docs | tags | created | target_merge | status_label | id | kind | project | work_item | inherits | refines | overrides | depends_on | imports | delegates | accepts_delegations | contract_packet | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| branch / feature-redis-atomic-program-catalog-contract | branch-note | raw | feature-redis-atomic-program-catalog-contract |
|
|
|
2026-07-28 | in-progress | BR-CA-SKELETON-OPERATIONAL-CONTRACT-062 | project-work-item | ca-skeleton-operational-contract | WI-CA-SKELETON-OPERATIONAL-CONTRACT-062 |
|
|
1 |
branch: feature-redis-atomic-program-catalog-contract
Layer:
raw/branch-notes/— 단일 브랜치의 TODO·결정·진행 기록. 머지/종료 후 verified 결과는/ingest로wiki/projects/에 추출. 원본은 raw에 영구 보관.status_label:in-progress|review|merged|abandoned스캐폴딩 상태 — 결정(D-row)·구현 가이드는 비어 있다./branch-spec feature-redis-atomic-program-catalog-contract로 채운다.
부모 (필수)
- Parent project (canonical SSOT): raw/project-notes/ca-skeleton-operational-contract
분해 근거:
docs/superpowers/specs/2026-07-28-ca-skeleton-production-capability-feature-decomposition-design.md§4.2 — 기술 런타임 (Tier T). 본 branch 는 project §8.0WI-CA-SKELETON-OPERATIONAL-CONTRACT-062의 실행 단위다.
형제 branch (같은 부모의 다른 자식 — 인접 영역):
- raw/branch-notes/feature-redis-runtime-role-isolation-contract
- raw/branch-notes/feature-edge-rate-limit-policy-registry-contract
- raw/branch-notes/feature-fenced-lock-coordination-contract
브랜치 계약 패킷
project Work Item 에서 내려온 실행 계약의 snapshot. 여기에는 pinned pointer + 1줄 요약 + branch 적용점만 쓰고 상세를 복제하지 않는다.
- 생성 시 프로젝트 개정:
1 - 패킷 스키마:
contract_packet: 1 - 완료 조건: program descriptor·golden vector·NOSCRIPT 재적재 test 와 unsafe recipe 경합 재현 test 가 통과한다
상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-REDIS-PROGRAM-001@1 |
다단계 read-decide-write는 버전 있는 atomic program으로만 제공하고 각 helper는 unsafe recipe와 안전한 대체를 쌍으로 문서화하며 경합 test로 증명한다 | /branch-spec 에서 적용 경계 작성 |
raw/project-notes/ca-skeleton-operational-contract |
브랜치 지역 결정
상세 근거와 선택 조건은 아래 결정-근거 매핑 표의 동일 D-row 가 소유한다. 여기에는 1줄 요약만 둔다.
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|---|---|---|---|
| D1 | atomic program 본문의 SSOT 는 .lua classpath resource 파일이고, 실행 메타데이터는 별도 descriptor 가 소유한다 |
refines DEC-…-REDIS-PROGRAM-001@1 |
spring-data-redis-scripting-execution.md#SDR-SCRIPT-C6·#SDR-SCRIPT-C5 |
proposed |
| D2 | EVALSHA 우선 + NOSCRIPT 를 받은 그때만 EVAL 로 1회 폴백. Spring Data Redis 에 위임하지 않고 Lettuce 위에 직접 구현한다 |
local |
as-built RedisLuaProgramExecutor.execute(), redis-eval-scripting-atomicity.md#REDIS-EVAL-C3 |
actually-implemented |
| D3 | pipeline 경로에서는 EVALSHA 를 쓰지 않고 plain EVAL 로 되돌린다 | local |
redis-eval-scripting-atomicity.md#REDIS-EVAL-C4 |
proposed |
| D4 | bounded execution 은 descriptor 선언 + 빌드타임 정적 검사를 1차 강제로 두고, 서버 실행시간 설정·kill 런북은 상시 병행하는 사후 안전망으로 분리한다 | refines DEC-…-REDIS-PROGRAM-001@1 |
redis-eval-scripting-atomicity.md#REDIS-EVAL-C1·#REDIS-EVAL-C6, redis-functions-library-deployment.md#REDIS-FN-C4 |
proposed |
| D5 | 모든 key 는 KEYS 로 선언하고, 미선언 key·프로그램 생성 key 접근을 program 작성 규칙으로 금지한다 |
local |
redis-eval-scripting-atomicity.md#REDIS-EVAL-C5, redis-lua-api-determinism-replication.md#REDIS-LUAAPI-C5 |
proposed |
| D6 | 배포 모드 기본값은 EVAL script 이고 Redis Functions 는 3조건 동시 충족 시에만 opt-in | local |
redis-functions-library-deployment.md#REDIS-FN-C1·#REDIS-FN-C2·#REDIS-FN-C6, redis-eval-scripting-atomicity.md#REDIS-EVAL-C2 |
proposed |
| D7 | unsafe recipe ↔ 대체 쌍은 읽은 값 기반 분기 유무로 갈라 등재한다 (분기 없음 → MULTI/EXEC, 분기 있음 → atomic program) |
refines DEC-…-REDIS-PROGRAM-001@1 |
redis-incr-rate-limiter-pattern.md#REDIS-INCR-C2·#REDIS-INCR-C3·#REDIS-INCR-C4 |
proposed |
| D8 | typed facade 는 semantics-free — program 식별자·KEYS·ARGV·반환 타입만 다루고 능력 의미(rate limit·lock·idempotency)를 알지 않는다 | local |
spring-data-redis-scripting-execution.md#SDR-SCRIPT-C3·#SDR-SCRIPT-C4 + 소비 branch 4건의 out-of-scope 선언 |
proposed |
| D9 | 경합 증명은 2층 분리 — unsafe recipe 의 race 는 fake + 결정론적 인터리빙(focused), atomic 대체의 원자성은 실제 Redis 통합 test | local |
test-taxonomy-testcontainers-official.md#TC-OFFICIAL-C3·#TC-OFFICIAL-C5, redis-eval-scripting-atomicity.md#REDIS-EVAL-C1 |
proposed |
| D10 | program 실행 실패는 backend 실패와 program 결함으로 등급을 나눈다 (결함은 miss 로 위장 금지) | local (hub §11 매트릭스 consume) |
redis-lua-api-determinism-replication.md#REDIS-LUAAPI-C1·#REDIS-LUAAPI-C2 |
proposed |
| D11 | program 실행에 bounded cardinality metric 1개 + 실행당 trace span 1개를 짝지어 붙이고, key·ARGV 는 절대 tag/attribute 로 넣지 않는다 | local (feature-metrics-alerting-contract D2·D8 consume) |
raw/branch-notes/feature-metrics-alerting-contract D2·D8 + metrics.yaml 의 lock.acquisition 선례 |
proposed |
선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|
가져온 프로젝트 계약
| Ref | Owner | 요약 | Branch 적용 |
|---|
목표
-
WI-CA-SKELETON-OPERATIONAL-CONTRACT-062의 완료 조건을 구현한다: program descriptor·golden vector·NOSCRIPT 재적재 test 와 unsafe recipe 경합 재현 test 가 통과한다 -
이슈:
-
PR:
범위
포함 범위
- atomic program 카탈로그와
ScriptDescriptor필수 필드 - EVALSHA 실행 + NOSCRIPT 안전 재적재
- bounded execution 규칙 (O(1)/유한, KEYS 선언, 무한 루프·대량 스캔 금지)
- unsafe multi-command recipe ↔ atomic 대체의 쌍 문서화와 경합 test 의무
- Redis Functions 배포 모드의 opt-in 조건
- golden vector 와 typed facade
제외 범위
의도적으로 제외한 것. 면접 등에서 "이건 범위에 없었습니다"라고 답할 근거.
- 능력별 상태 프로그램의 의미 — 각 능력 branch 소유
- Redis 연결·키·코덱 — #061 소유
- 심층 명세 — 본 노트는 스캐폴딩. D-row 와 §구현 가이드는
/branch-spec이 채운다. - project decision registry 변경 — owner 는 project-note
근거 (필수, 최소 1개+)
| Source | 정당화하는 결정 |
|---|---|
| raw/official-docs/redis-eval-scripting-atomicity | 다단계 read-decide-write 는 EVALSHA 로 실행하는 버전 있는 atomic program 으로만 제공하고, NOSCRIPT 시 안전 재적재하며, bounded execution(유한 실행) 규칙을 둔다 — 스크립트 원자성(블로킹 실행)·EVALSHA/NOSCRIPT 복구·KEYS 선언 규칙·SCRIPT KILL 근거 |
| raw/official-docs/spring-data-redis-scripting-execution | ⚠️ 채택하지 않은 대안의 근거 — 초판은 이 문서를 D1·D2 의 기반으로 삼았으나 as-built 에 spring-data-redis 의존성이 없어 폐기했다. 현재는 "프레임워크에 위임했다면 어땠을지" 의 비교 기준(#SDR-SCRIPT-C1 의 evalsha→eval 자동 폴백)으로만 남는다. #SDR-SCRIPT-C6(script 를 classpath resource 로 두는 패턴)만 D1 에서 여전히 유효 |
| raw/official-docs/redis-lua-api-determinism-replication | call/pcall 오류 전파 차이, typed error/status reply(redis.error_reply/redis.status_reply), effects replication(Redis 7.0 기준 유일 모드) + redis.replicate_commands() 폐기 이력, KEYS 선언 강제, sandbox 제약(전역 변수 금지·require 비활성화)의 공식 근거 |
| raw/official-docs/redis-functions-library-deployment | Redis Functions(FUNCTION LOAD/LIST, Shebang 선언, no-writes flag, Redis 7.0 버전 요구, cluster 수동 배포)의 opt-in 조건을 무엇으로 삼을지의 공식 근거 |
| raw/official-docs/redis-incr-rate-limiter-pattern | unsafe multi-command recipe (GET→판단→INCR→조건부 EXPIRE) 의 race condition 을 공식 문서가 명시 — 각 atomic program helper 가 unsafe recipe 와 안전한 atomic 대체(MULTI/EXEC 또는 Lua/EVAL)를 쌍으로 문서화해야 한다는 결정의 근거 |
| raw/official-docs/redis-programmability-max-execution-time | bounded execution 규칙을 예방 계층(정적 검사)과 사후 대응 계층(서버 설정 + kill 런북)으로 분리해 상시 병행한다는 결정(D4)의 세부 근거 — 기본 5초 임계값·busy-reply-threshold 설정명, 시간 초과 시 자동 종료하지 않는 이유(atomicity 계약 보호), BUSY 에러 상태의 허용 명령(SCRIPT KILL/FUNCTION KILL/SHUTDOWN NOSAVE), 쓰기 수행 스크립트는 SHUTDOWN NOSAVE 만 허용됨을 공식 확인 |
| raw/official-docs/test-taxonomy-testcontainers-official (재사용 — owner 는 feature-test-taxonomy-fixture-contract) | D9 의 "atomic 원자성은 실제 Redis 로만 증명" — #TC-OFFICIAL-C3(mock/in-memory 없이 production 과 동일 type 서비스), #TC-OFFICIAL-C5(in-memory 는 production service 의 기능을 다 갖지 않음). ⚠️ 원문의 예시는 H2 이고 Redis·Lua 를 직접 말하지 않는다 — "임베디드 Redis 는 Lua 를 온전히 못 돌린다" 는 이 claim 의 유추이지 인용이 아니다. D9 자체는 #REDIS-EVAL-C1+#TC-OFFICIAL-C3 로 닫힌다 |
추가 수집 완료 (busy-reply-threshold/BUSY/SHUTDOWN NOSAVE bounded-execution 세부 설정값): [[raw/official-docs/redis-programmability-max-execution-time]] (develop/programmability/ §Maximum execution time)로 닫혔다 — REDIS-PROG-C1~C4. D4 의 Supporting Claims·Open Risk 와 §구현 가이드 3 의 (3)층 표, §엣지·실패·의존에 이미 반영 완료(2026-07-28 동일 /branch-spec 실행). 구 파라미터명 lua-time-limit 과 maxmemory 상호작용은 이 자료에도 없어 미확인으로 남으며, 후자는 §Claims To Verify 에 등록했다.
미수집 — 결정에 사용하지 않음: LINE engineering 의 "Lua 채택 + EVALSHA 의도적 거부" 사례(§Audit EVALSHA_COUNTEREXAMPLE). D2 의 반례 후보이나 raw archive 전이므로 인용하지 않았다.
프로젝트 내부 설계 참조 (등급 internal-design-doc — 공식 문서 아님, best practice 로 격상 금지):
- ca-tmpl
docs/superpowers/specs/2026-07-26-production-capability-platform-design.md - llm-wiki
docs/superpowers/specs/2026-07-28-ca-skeleton-production-capability-feature-decomposition-design.md
TODO
각 항목 옆에 증거 등급 표기: actually-implemented | locally-verified | prod-verified | documented-only | planned | needs-confirmation
/branch-spec로 D-row(D1~D11)·§구현 가이드(9절) 작성 — 등급:documented-only.lua카탈로그 + descriptor + manifest 대조 test (D1) — 등급:actually-implemented(RedisProgramCatalog·RedisProgramDescriptor·program-set.json)- EVALSHA + NOSCRIPT EVAL 폴백 (D2) — 등급:
actually-implemented(RedisLuaProgramExecutor+ test 2건) - 런타임 admission — arity·바이트 상한·카탈로그 소유권 (D4 (i)층) — 등급:
actually-implemented(RedisLuaProgramExecutor.validate()) - typed facade 2층 구조 (D8) — 등급:
actually-implemented(RedisAtomicPrimitives+RedisProgramExecutorseam) - §1·§2·§6·§7 을 as-built 로 정정 — 초판 Spring Data Redis 전제 잔존 (§구현 가이드 preamble 표 참조) — 등급:
planned 2026-07-26-redis-production-capability-design.md(6,700줄) 정독 후UNSUPPORTED_IMPL_DECISION라벨 재판정 — 등급:planned- unsafe recipe 경합 재현 test — WI-062 완료 조건 중 유일한 미구현분 (D9) — 등급:
planned - bounded execution 빌드타임 정적 검사 + denylist 시드 (D4 (ii)층) — 등급:
planned - unsafe recipe ↔ 대체 쌍 카탈로그 2행 등재 (D7) — 등급:
planned - program 실패 error code + metric·span registry 등재 제안 (D10·D11) — 등급:
planned - 선행 확인: 배포 이미지의 실제
redis_version≥ 7.2 — 등급:needs-confirmation - harness 정정 요청 전달:
/branch-spec명령 문서의 ca-tmpl ground truth 경로 (§AuditSTALE_CLONE_ROOT_CAUSE) — 등급:needs-confirmation
진행 중 메모
- 2026-07-28 — 본 branch 는 greenfield 가 아니다. foundation 이 이미 구현돼 있다(
RedisProgramCatalog·RedisProgramDescriptor·RedisProgramExecutor·RedisLuaProgramExecutor·RedisAtomicPrimitives+.lua3개 +program-set.json+ test 7개). 착수는 "신규 작성" 이 아니라 "as-built 확장" 으로 계획할 것. - ⚠️ 초판의 3중 오판을 정정했다 — "코드 없음 / Redis 버전 미고정 / 설계문서 부재" 는 전부 낡은 clone(
workspace/ca-tmpl)을 조회한 결과였다. 실제 ground truth 는desktop-server-git/clean-architecture-backend-template/다(§AuditSTALE_CLONE_ROOT_CAUSE). - SDK 는
io.lettuce:lettuce-core단독이며spring-data-redis는 없다. Redis 최소 버전은program-set.json의minimumRedisVersion: "7.2"로 고정돼 있다. - 다음 세션 우선 작업:
docs/superpowers/specs/2026-07-26-redis-production-capability-design.md(6,700줄) 정독. 이 문서가 golden vector 형식·BUSY 관측·Functions 호환·버전 pin 을 직접 다루므로, 현재UNSUPPORTED_IMPL_DECISION으로 남긴 항목 상당수가 근거를 얻을 가능성이 높다.
결정 사항
- 2026-07-28: atomic program 본문은
.luaclasspath resource, 메타데이터는 별도 descriptor (D1) / 이유: Spring 공식 예시가ResourceScriptSource+ClassPathResource를 표준 패턴으로 제시하고, SHA1 이 본문에서 자동 도출돼 "변경 시 버전 변경" 을 별도 규약 없이 만족 / 검토한 대안: Java 인라인 문자열 상수(LINE 프로덕션 선례 있음, 다만 미archive), Redis Functions library 파일, 빌드타임 코드생성 / 근거: raw/official-docs/spring-data-redis-scripting-execution - 2026-07-28: EVALSHA 우선 +
NOSCRIPT시에만 EVAL 1회 폴백을 Lettuce 위에 직접 구현 (D2, 등급actually-implemented) / 이유: as-built 가 이미 이 형태이고spring-data-redis의존성이 없다 / 검토한 대안: Spring Data RedisScriptExecutor위임 — SDK 를 새로 들여야 하고 #061 이 raw Lettuce 로 기울어 채택하지 않음 / 위험: 공식 절차(SCRIPT LOAD+재시도)와 경로가 달라 캐시 재적재를 코드가 관측하지 않음 — §Claims To Verify 등록 / 근거: raw/official-docs/redis-eval-scripting-atomicity + as-builtRedisLuaProgramExecutor - 2026-07-28: bounded execution 은 예방(정적)과 사후 대응(서버 설정)을 분리해 둘 다 채택 (D4) / 이유: 쓰기를 수행한 program 은
SCRIPT KILL로 죽일 수 없어(공식) 예방이 필수이고, 데이터 규모에서만 드러나는 초과는 정적 검사로 원천 재현 불가라 사후 층이 필수 / 검토한 대안: 서버 설정만, 리뷰 체크리스트만 / 근거: raw/official-docs/redis-eval-scripting-atomicity - 2026-07-28: 배포 모드 기본은 EVAL script, Functions 는 3조건 opt-in (D6) / 이유: Functions 는 Redis 7.0+ 필요한데 대상 버전이 확정돼 있지 않다(§Audit
NO_VERSION_PIN) / 검토한 대안: Functions 를 SSOT 로 채택 — SHA 기반 버전 식별이 성립하지 않아 보류 / 근거: raw/official-docs/redis-functions-library-deployment - 2026-07-28: unsafe 쌍은 분기 유무로 분리 등재 (D7) / 이유: 공식이 분기 없는 recipe 에는
MULTI/EXEC, 분기 있는 recipe 에는 Lua 를 서로 다르게 권고한다. 한 항목으로 뭉치면 이 구분이 사라진다 / 근거: raw/official-docs/redis-incr-rate-limiter-pattern - 2026-07-28: 경합 증명을 focused / integration 2층으로 분리 (D9) / 이유: race 는 클라이언트 측 순서 문제라 fake+barrier 로 결정론적 재현이 가능하지만, 원자성은 서버가 보장하는 성질이라 실제 Redis 없이는 증명 불가 / 제약: focused 레벨 Testcontainers 금지는 raw/branch-notes/feature-test-taxonomy-fixture-contract 가 ArchUnit 으로 이미 강제 / 근거: raw/official-docs/redis-eval-scripting-atomicity · raw/official-docs/test-taxonomy-testcontainers-official
Decision Evidence Map / 결정-근거 매핑
2026-07-28
/branch-spec조사(Redis 공식 6건 + Spring Data Redis 1건 + 기존 Testcontainers raw 재사용)로 채웠다. 본 branch 는 greenfield 가 아니다 — foundation 이 이미 구현돼 있다. 실제 ground truth 는/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/이며(§AuditGROUND_TRUTH_PATH_DRIFT),adapter/outbound/cache-redis에RedisProgramCatalog·RedisProgramDescriptor·RedisProgramExecutor·RedisLuaProgramExecutor·RedisProgramId+.lua3개 +program-set.jsonmanifest + 전용 test 7개가 존재한다(등급actually-implemented). 따라서 아래 D-row 는 as-built 를 서술하거나(등급 표기) as-built 를 확장하는 결정이다. 초기 작성 시planned로 적었던 것을 코드 대조 후 정정했다.
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | atomic program 본문의 SSOT 는 .lua classpath resource, 식별·서명은 닫힌 enum 카탈로그(RedisProgramId → RedisProgramDescriptor)가 소유하고, 외부 노출은 program-set.json manifest 가 미러링한다 |
항상. D6 이 Functions 모드로 뒤집히면 SSOT 가 library 파일(#!lua name=…)로 이동한다 — redis-functions-library-deployment.md#REDIS-FN-C3 |
raw/official-docs/spring-data-redis-scripting-execution.md#SDR-SCRIPT-C6(script 본문을 classpath resource 로 두는 패턴) + as-built: RedisProgramId(3 program, <name>-v1 + redis/scripts/*.lua), RedisProgramCatalog.foundation(), redis/program-set.json |
official-vendor-doc + actually-implemented |
as-built 는 버전을 id 문자열(-v1)에 넣고 SHA-256 을 source digest 로 따로 보관한다 — 내가 처음 적은 "SHA1 이 곧 버전" 은 as-built 와 다르다(SHA-1 은 EVALSHA 전송용으로만 쓰임). manifest 와 카탈로그의 drift 는 unit test 가 잡지만, .lua 와 descriptor 의 arity drift 는 실행 시점에만 드러난다 |
| D2 | EVALSHA 우선 + NOSCRIPT 를 받은 그때만 EVAL 로 1회 폴백. Spring Data Redis 에 위임하지 않고 Lettuce 위에 직접 구현한다 |
항상 — as-built 가 이미 이 형태다. Spring Data Redis 를 도입하면 ScriptExecutor 위임으로 대체 가능하나, #061 이 raw Lettuce 유지로 기운 상태라 전환 근거가 없다 |
raw/official-docs/redis-eval-scripting-atomicity.md#REDIS-EVAL-C3(NOSCRIPT 복구 절차) + as-built: RedisLuaProgramExecutor.execute() 가 commands.evalSha(...) → catch RedisNoScriptException → commands.eval(...). test fallsBackToEvalOnlyWhenEvalShaReportsNoScript·doesNotEvalAgainWhenCachedScriptExecutes |
official-vendor-doc + actually-implemented |
as-built 는 SCRIPT LOAD 를 쓰지 않고 EVAL 직접 실행으로 복구한다. #REDIS-EVAL-C3 이 서술한 "SCRIPT LOAD 후 EVALSHA 재시도" 와 다른 경로이며, 결과적으로 캐시 재적재 여부를 코드가 관측하지 않는다(EVAL 이 부수적으로 캐시에 넣음). 기능상 동등하나 공식이 명시한 절차는 아님 |
| D3 | pipeline 경로에서는 EVALSHA 를 쓰지 않고 plain EVAL 로 되돌린다 | program 을 pipeline 안에서 실행할 때. 단건 실행이면 D2 의 EVALSHA 경로 | raw/official-docs/redis-eval-scripting-atomicity.md#REDIS-EVAL-C4(pipeline 안에서는 NOSCRIPT 가 반환돼도 handle 할 수 없으므로 plain EVAL 로 revert 해야 함) |
official-vendor-doc |
근거의 구속력이 should(권고)이지 MUST 가 아니다(#REDIS-EVAL-C4 의 does-not-prove). 또 Spring 의 ScriptExecutor 가 pipeline 안에서 이 revert 를 자동으로 하는지는 #SDR-SCRIPT-C1 이 말하지 않는다 |
| D4 | bounded execution 은 (i) descriptor 선언 + 런타임 admission(as-built) + (ii) 빌드타임 정적 검사(신규)를 1차 강제로, (iii) 서버 실행시간 설정 + kill 런북을 상시 병행 안전망으로 분리 | (i)은 arity·바이트 상한·카탈로그 소유권처럼 호출 시점에 판정 가능한 위반에 적용(as-built 가 이미 강제). (ii)는 .lua 텍스트의 정적 위반(비유한 명령 토큰·무한 루프 리터럴)에 적용 — as-built 에 없음. 운영 데이터 규모에서만 드러나는 초과는 (i)(ii)로 원천 재현 불가하므로 (iii)이 담당 — (iii)은 선택이 아니라 필수 병행 |
raw/official-docs/redis-eval-scripting-atomicity.md#REDIS-EVAL-C1(실행 중 모든 서버 활동 블로킹), #REDIS-EVAL-C6(SCRIPT KILL 은 dataset 을 수정하지 않은 스크립트에만 가능), raw/official-docs/redis-functions-library-deployment.md#REDIS-FN-C4(Functions 도 동일 블로킹), raw/official-docs/redis-programmability-max-execution-time.md#REDIS-PROG-C1~#REDIS-PROG-C4(기본 5초 busy-reply-threshold · 자동 종료 안 함 · BUSY 허용 명령 · 쓰기 후엔 SHUTDOWN NOSAVE 뿐) |
official-vendor-doc ×3 |
쓰기를 수행한 program 의 유일한 종료 수단이 SHUTDOWN NOSAVE(인스턴스 강제 정지) 라는 것이 강제가 필요한 진짜 이유다(#REDIS-PROG-C4). 남은 미확인: 구 파라미터명 lua-time-limit 과의 관계, maxmemory 근접 시 script abort 동작 — 두 자료 모두에 없다 |
| D5 | 모든 key 는 KEYS 입력 인자로 선언. 프로그램 생성 key·데이터 내용 기반 key 접근을 program 작성 규칙으로 금지 |
항상 — standalone·cluster 무관. cluster 에서는 추가로 같은 slot 으로 해싱되어야 multi-key 연산이 성립한다 | raw/official-docs/redis-eval-scripting-atomicity.md#REDIS-EVAL-C5, raw/official-docs/redis-lua-api-determinism-replication.md#REDIS-LUAAPI-C5 (양쪽 모두 "should only access keys given as input arguments" / "should never access programmatically-generated names") |
official-vendor-doc ×2 (독립 페이지 2곳) |
미선언 key 를 실제로 접근했을 때의 거부 동작·에러 코드가 공식에 없다(#REDIS-EVAL-C5 does-not-prove) — 즉 Redis 가 막아주지 않으므로 우리가 강제하지 않으면 조용히 통과한다. hash slot 라우팅 자체는 #061 소유 |
| D6 | 배포 모드 기본값은 EVAL script. Redis Functions 는 ① 대상 Redis 7.0+ 확정 ② NOSCRIPT 재적재 부담 제거가 실제 문제로 관측 ③ cluster 전 노드 수동 적재 운영을 수용 — 3조건 동시 충족 시에만 opt-in | 위 3조건 중 하나라도 미충족이면 EVAL script. 특히 대상 Redis 버전이 확정되지 않은 현재 상태에서는 Functions 를 고를 수 없다 | raw/official-docs/redis-functions-library-deployment.md#REDIS-FN-C1(7.0+ 필요), #REDIS-FN-C2(Functions 는 AOF 영속+복제 / script 는 휘발성 캐시), #REDIS-FN-C6(cluster 전 노드 적재는 자동화되지 않음), raw/official-docs/redis-eval-scripting-atomicity.md#REDIS-EVAL-C2(캐시는 재시작·failover·SCRIPT FLUSH 시 소실) |
official-vendor-doc |
ca-tmpl 어디에도 Redis 버전이 고정돼 있지 않다 — compose 에 redis 서비스 없음, SDK 의존성 없음(§Audit NO_VERSION_PIN). 조건 ①을 지금 판정할 수 없다. #REDIS-FN-C2 의 durability 가 appendonly no 배포에서도 성립하는지도 미서술 |
| D7 | unsafe recipe ↔ 안전한 대체 쌍은 읽은 값 기반 분기 유무로 갈라 등재한다 | 분기 없음(고정 키 카운터 등) → MULTI/EXEC 로 충분하며 program 을 만들지 않는다. 분기 있음(IF value == 1 류) → MULTI/EXEC 로 불충분하므로 atomic program 필수 |
raw/official-docs/redis-incr-rate-limiter-pattern.md#REDIS-INCR-C4(공식이 두 변형에 서로 다른 대체를 권고), #REDIS-INCR-C2(rate limiter 2 에 race condition 존재를 공식이 명시), #REDIS-INCR-C3(실패 모드 = EXPIRE 유실 → key leak) |
official-vendor-doc |
이 분기 기준을 잃고 "INCR+EXPIRE" 를 한 항목으로 뭉치면 #REDIS-INCR-C4 의 구분이 사라진다. 또 공식은 key leak 의 정량적 심각도를 말하지 않는다(정성 서술만) |
| D8 | typed facade 는 semantics-free — program 식별자·KEYS·ARGV·상태 문자열만 다루고 능력 의미를 알지 않는다. descriptor·catalog·executor 는 package-private 로 유지해 adapter 밖으로 새지 않는다 | 항상. 능력 의미(rate limit 판정·lock fencing·idempotency claim 결과)가 facade 에 새어들면 소비 branch 의 owner 경계를 침범한다 | as-built: RedisProgramExecutor javadoc — "Implementations may use Functions or EVALSHA, but application code must only depend on semantic ports and typed facades". 전 program 타입이 package-private. 반환은 descriptor.statuses() 에 속한 ASCII 상태 문자열 |
actually-implemented |
as-built 반환 타입은 #SDR-SCRIPT-C4 의 4종이 아니라 상태 문자열 단일 형태다(≤128 byte). 값을 함께 돌려줘야 하는 program(예: 남은 quota)이 생기면 이 형태로 부족하고, 그 확장 형식은 근거가 없다 |
| D9 | 경합 증명을 2층으로 분리 — unsafe recipe 의 race 는 fake + 결정론적 인터리빙으로 focused test, atomic 대체의 원자성은 실제 Redis 통합 test | 원자성 주장은 서버가 보장하는 성질이므로 fake 로 증명 불가 → 실제 Redis 필수. 반대로 unsafe recipe 의 인터리빙은 클라이언트 측 순서 문제이므로 fake 로 결정론적 재현이 가능하고, 그래야 focused test 의 "실제 네트워크 금지" 규칙을 지킨다 | raw/official-docs/redis-eval-scripting-atomicity.md#REDIS-EVAL-C1(원자성은 서버 보장), raw/official-docs/test-taxonomy-testcontainers-official.md#TC-OFFICIAL-C3(mock/in-memory 없이 production 과 동일 type 서비스로 test), #TC-OFFICIAL-C5(in-memory 는 production service 의 기능을 다 갖지 않음) |
official-vendor-doc + official-reference |
WI-062 완료 조건의 미충족분이 정확히 여기다. as-built 는 NOSCRIPT 재적재 test 와 golden-vector 성격 test 를 이미 갖췄지만(fallsBackToEvalOnlyWhenEvalShaReportsNoScript·loadsEveryFoundationProgramWithAnExactDigestAndBoundedSignature), unsafe recipe 경합 재현 test 는 모듈에 존재하지 않는다(CountDownLatch/Thread grep 0건). 반복 횟수·스레드 수의 공식 근거도 없다 |
| D11 | program 실행에 bounded cardinality metric 1개(redis.program.executions.total, tag = program_id+outcome)와 실행당 trace span 1개를 짝지어 붙이고, key·ARGV 는 tag/attribute 로 넣지 않는다 |
항상. program_id 를 tag 로 쓰는 것은 카탈로그가 닫힌 enum 이라 cardinality 가 구조적으로 유한할 때만 유효하다 — 런타임 등록으로 카탈로그가 열리면 이 tag 를 빼야 한다 |
raw/branch-notes/feature-metrics-alerting-contract D2(Micrometer dot.case 명명)·D8(high-cardinality tag 금지 + bounded whitelist, Prometheus·Micrometer 공식 근거 보유) + metrics.yaml 의 lock.acquisition 선례("key 는 tag 로 넣지 않는다") + as-built 닫힌 EnumMap 카탈로그 |
project-registry-contract (owner 는 metrics-alerting) + actually-implemented(카탈로그 유한성) |
metric 이름·outcome 어휘·cardinality 상한은 공식 근거가 없다(§구현 가이드 9 에서 UNSUPPORTED_IMPL_DECISION 라벨). metrics.yaml 에 script/program row 가 0건이므로 전부 신규 제안이며 registry-governance 절차를 거쳐야 한다. BUSY 상태는 서버 측 현상이라 이 클라이언트 metric 으로 직접 관측되지 않는다 — D4 (iii)층의 신호는 별도다 |
| D10 | program 실행 실패를 backend 실패와 program 결함으로 등급 분리. 결함은 해당 연산을 fail closed 하고 miss 로 위장하지 않는다 | backend 연결·timeout 실패 → 기존 CACHE_UNAVAILABLE(TRANSIENT_DEPENDENCY) 선례를 따름. program 결함(Lua 런타임 오류·반환 계약 위반) → fail closed. 이 경계는 hub §11 매트릭스의 "cache codec·프로그래밍 결함은 fail closed, miss 로 위장 금지" 와 동일 규칙 |
raw/official-docs/redis-lua-api-determinism-replication.md#REDIS-LUAAPI-C1(redis.call 은 예외를 그대로 올려 스크립트 중단 / redis.pcall 은 절대 throw 하지 않고 error 테이블 반환), #REDIS-LUAAPI-C2(redis.error_reply/redis.status_reply 가 typed 반환의 공식 수단) |
official-vendor-doc (등급 분리 규칙 자체는 hub §11 이 owner) |
신규 error code 가 필요하다 — docs/registries/error-codes.yaml 에 script/program 관련 code 가 하나도 없다(grep 확인). 신규 code 는 registry-governance 절차를 거쳐야 하고 owner 는 본 branch 다. Category enum 10종 밖의 값은 만들 수 없다(shared/error/Category.java 확인) |
구현 가이드
2026-07-28
/branch-spec조사로 in-scope detail 을 채웠다. 근거가 원칙만 지지하고 detail 은 지지하지 않는 cell 은UNSUPPORTED_IMPL_DECISION+ trade-off 한 줄로 표시했다(CLAUDE.md §15.5 R2). 다른 branch 결정 영역은 남기지 않았다(R3).⚠️ 부분 재작성 상태 (다음 세션 최우선) — §3·§8·§9 는 as-built 로 재작성했으나 §1·§2·§6·§7 은 아직 초판의 Spring Data Redis 전제가 남아 있다. 아래 as-built 사실과 충돌하는 서술이 그 4개 절에 있으므로, 착수 전 반드시 정정해야 한다:
as-built 사실 근거 충돌하는 절 리소스 경로는 redis/scripts/*.lua(META-INF/scripts/아님)RedisProgramId§1 로딩은 ClassLoader.getResourceAsStream+RedisProgramCatalog.foundation()(ResourceScriptSource/DefaultRedisScript아님)RedisProgramCatalog§1·§2 descriptor 실제 필드 8개 = id·sha256·scriptBytes·keyCount·argumentCount·maximumKeyBytes·maximumArgumentBytes·statusesRedisProgramDescriptor§1 manifest 에 complexity: "O(1)"·timeoutCertainty가 이미 있다 — §3 (1)층의writes신설 전에 이 필드 재사용을 먼저 검토program-set.json§3 실제 Redis lane 은 Testcontainers 가 아니다 — @Tag("redis-service")+./gradlew :adapter:outbound:cache-redis:redisServiceTest+-Dredis.test.host/-Dredis.test.port. 모듈에 Testcontainers 참조 0건build.gradle,LettuceRedisRuntimeServiceTest§6 typed facade 가 이미 있다 — RedisAtomicPrimitives의compareAndDelete/compareAndExpire/setIfAbsentWithTtl, 반환은 program 별 typed enum. 즉 2층 구조(executor seam = 상태 문자열 / facade = typed enum)RedisAtomicPrimitives§7·D8 모듈 좌표(
actually-implemented): idadapter-outbound-cache-redis, Gradle:adapter:outbound:cache-redis, package rootdev.caskeleton.adapter.outbound.cache. 이 저장소는 harness-free 라.harness/project/modules.yaml이 없다 — 초판의 그 인용은 낡은 clone 기준이었다.
1. program 카탈로그와 ScriptDescriptor
Trace: D1 ←
spring-data-redis-scripting-execution.md#SDR-SCRIPT-C6(classpath resource 표준 패턴)·#SDR-SCRIPT-C5(단일 인스턴스로 SHA1 재계산 회피)
- UNSUPPORTED_IMPL_DECISION: descriptor 를 YAML registry 로 둘지 Java enum 으로 둘지.
#SDR-SCRIPT-C6은 script 본문을 classpath resource 로 두라고만 말하고 메타데이터의 소재를 말하지 않는다. trade-off: 이 repo 는 계약 값을docs/registries/*.yaml로 관리하는 관행이 이미 있어(error-codes·env-keys·metrics) YAML 을 택하면 검증 스타일을 재사용할 수 있다. 대신.lua파일과 YAML 행이 물리적으로 분리돼 drift 가 생기므로 §3 의 정적 검사가 두 쪽을 대조해야 한다.- UNSUPPORTED_IMPL_DECISION: program id 명명 규칙. 공식 근거 없음. trade-off: 카탈로그가 능력 의미를 알면 안 되므로(D8)
rate-limit-token-bucket처럼 능력 이름을 쓰지 않고incr-with-ttl-if-absent처럼 연산을 서술하는 중립 이름을 쓴다.
| 항목 | planned 명세 | 근거 |
|---|---|---|
| script 본문 | .lua 파일 1개 = program 1개. META-INF/scripts/ 아래 classpath resource |
#SDR-SCRIPT-C6 |
| 로딩 | ResourceScriptSource(new ClassPathResource(...)) → RedisScript.of(source, <resultType>) |
#SDR-SCRIPT-C6 |
| 인스턴스 수명 | program 당 DefaultRedisScript 를 컨텍스트에 단일 인스턴스로 등록 — 매 실행 SHA1 재계산 금지 |
#SDR-SCRIPT-C5 |
| descriptor 필수 필드 | programId · resourcePath · keyCount · argSchema · resultType · writes(boolean) · ownerBranch |
UNSUPPORTED_IMPL_DECISION (필드 집합은 공식 근거 없음). resultType 은 #SDR-SCRIPT-C4 의 4종으로 제한, writes 는 §3 이 요구 |
| 버전 식별 (3축 분리) | ① 계약 version = program id 의 -v1 접미사 — 호환 불가 변경 시 신규 id 로 올린다(소비자가 pin 하는 축). ② source digest = SHA-256, manifest 와 카탈로그 대조용. ③ 실행 식별자 = SHA-1, EVALSHA 전송용으로만 쓰이며 계약 의미 없음 |
as-built RedisProgramId(compare-and-delete-v1 등) · RedisProgramDescriptor.sha256() · RedisLuaProgramExecutor.sha1(). #SDR-SCRIPT-C5 는 SHA1 재계산 회피만 말하고 버저닝 전략은 다루지 않는다(그 문서의 does-not-prove) |
| golden vector | program 당 (KEYS, ARGV) → 기대 반환 고정 벡터 ≥1. descriptor 의 keyCount·resultType 과 대조 |
UNSUPPORTED_IMPL_DECISION (형식은 공식 근거 없음). trade-off: 벡터가 없으면 script 수정이 조용히 반환 계약을 깬다 |
2. 실행 경로 — 위임과 pipeline 예외
Trace: D2 ←
#SDR-SCRIPT-C1(evalsha 시도 → 미적재 시 eval 폴백)·redis-eval-scripting-atomicity.md#REDIS-EVAL-C3(NOSCRIPT 복구) / D3 ←#REDIS-EVAL-C4(pipeline 안에서는 NOSCRIPT 를 handle 못 함)
- UNSUPPORTED_IMPL_DECISION: 재적재 로직을 직접 짜지 않는다는 선택. 두 근거가 서로 다른 복구 경로를 말한다(Redis =
SCRIPT LOAD+재시도 / Spring =eval폴백). trade-off: 직접 구현하면 두 경로를 우리가 책임져야 하고, 위임하면 프레임워크 동작에 종속되지만 재적재 버그를 우리가 만들지 않는다. 어느 쪽이든 NOSCRIPT 재적재 test 는 실제 Redis 에서SCRIPT FLUSH후 재실행으로 증명해야 한다(§6).
| 경로 | planned 동작 | 근거 |
|---|---|---|
| 단건 실행 | execute(RedisScript, List<K> keys, Object... args) — EVALSHA 우선은 프레임워크가 처리 |
#SDR-SCRIPT-C1, #SDR-SCRIPT-C3 |
| 캐시 소실 | 재시작·failover·SCRIPT FLUSH 로 언제든 발생 가능. 애플리케이션이 복구 책임을 진다 |
#REDIS-EVAL-C2, #REDIS-EVAL-C3 |
| pipeline | EVALSHA 금지 — plain EVAL 로 되돌린다. pipeline 안에서는 NOSCRIPT 가 와도 처리할 수 없다 | #REDIS-EVAL-C4 |
| 반환 타입 | Long·Boolean·List·역직렬화 값 중 하나. OK 류 상태 반환은 null |
#SDR-SCRIPT-C4 |
3. bounded execution — 강제 3층
Trace: D4 ←
#REDIS-EVAL-C1(실행 중 서버 전면 블로킹)·#REDIS-EVAL-C6(SCRIPT KILL은 쓰기 안 한 스크립트만) / D5 ←#REDIS-EVAL-C5·redis-lua-api-determinism-replication.md#REDIS-LUAAPI-C5(KEYS 선언) / sandbox ←#REDIS-LUAAPI-C6(전역 선언 차단)·#REDIS-LUAAPI-C7(require비활성)
- UNSUPPORTED_IMPL_DECISION: 정적 검사를 ArchUnit 이 아니라 일반 JUnit test 로 구현. ArchUnit 의 검사 대상은 JVM 바이트코드이므로
.lua텍스트는 그 DSL 대상이 아니다. trade-off: 같은 test 스위트 안에 두어 실행 위치는 통일하되, 규칙은 텍스트 검사로 짠다.- UNSUPPORTED_IMPL_DECISION: 금지 토큰 목록(denylist)의 구체 항목과 파서 수준(정규식 vs Lua lexer). 공식 문서는 원칙만 말한다. trade-off: 정규식은 문자열 조립(
'S'..'MEMBERS')으로 우회되므로 완전한 강제가 아님을 문서에 명시하고, 우회는 리뷰가 맡는다.
| 층 | 무엇을 잡는가 | 무엇을 못 잡는가 | 근거 |
|---|---|---|---|
| (1) descriptor 선언 | keyCount·writes 를 program 이 스스로 선언 — 선언 없이는 카탈로그 등재 불가 |
선언 자체가 거짓인 경우 | D1 |
| (2) 빌드타임 정적 검사 | 하드코딩 key 리터럴, 비유한 명령 토큰, 무한 루프 리터럴, KEYS[] 미사용 |
문자열 조립 우회, 데이터 규모에 비례하는 사실상 무한 | #REDIS-EVAL-C5, #REDIS-LUAAPI-C5 |
| (3) 서버 설정 + kill 런북 | 운영 데이터 규모에서만 드러나는 실행 시간 초과 | 근본 원인(미선언 key·비유한 명령) 자체 — 예방이 아니라 사후 대응 | #REDIS-EVAL-C6, redis-programmability-max-execution-time.md#REDIS-PROG-C1~#REDIS-PROG-C4 |
(3)층의 확정된 값 (전부 official-vendor-doc):
| 항목 | 값 | 근거 |
|---|---|---|
| 최대 실행 시간 기본값 | 5초, 설정명 busy-reply-threshold (밀리초 단위, redis.conf 또는 CONFIG SET) |
#REDIS-PROG-C1 |
| 임계 초과 시 | Redis 가 자동 종료하지 않는다 — 종료하면 원자성 계약이 깨지므로 | #REDIS-PROG-C2 |
| 초과 후 서버 상태 | 다른 클라이언트에 BUSY 응답. 허용 명령은 SCRIPT KILL · FUNCTION KILL · SHUTDOWN NOSAVE 뿐 |
#REDIS-PROG-C3 |
| 쓰기를 이미 수행한 경우 | SHUTDOWN NOSAVE 만 가능 — 데이터셋을 디스크에 저장하지 않고 서버를 정지시킨다(사실상 강제 종료) |
#REDIS-PROG-C4, #REDIS-EVAL-C6 |
(3)이 선택이 아니라 필수 병행인 이유: 쓰기를 수행한 program 이 폭주하면 (1)(2)를 통과했더라도 죽일 수단이 SHUTDOWN NOSAVE — 인스턴스 강제 정지 뿐이다(#REDIS-PROG-C4). 즉 bounded execution 위반의 최종 비용이 가용성 사고이므로 예방 층을 생략할 수 없다. 런북에 이 3단계(BUSY 관측 → 쓰기 여부 판별 → kill 또는 SHUTDOWN NOSAVE)를 명시한다.
program 작성 규칙 (카탈로그 등재 조건):
| 규칙 | 내용 | 근거 |
|---|---|---|
| KEYS 선언 | 접근하는 모든 key 를 KEYS 인자로 받는다. 프로그램 생성 key·데이터 내용 기반 key 접근 금지 |
#REDIS-EVAL-C5, #REDIS-LUAAPI-C5 |
| 전역 금지 | 모든 선언은 local. 전역 변수·함수 선언은 샌드박스가 차단 |
#REDIS-LUAAPI-C6 |
| 모듈 금지 | require 비활성 — 허용된 런타임 라이브러리만 사용 |
#REDIS-LUAAPI-C7 |
| 복제 | effects replication 이 Redis 7.0 기준 유일 모드이며 정의상 결정론적이다. redis.replicate_commands() 를 호출하지 않는다 |
#REDIS-EVAL-C7, #REDIS-LUAAPI-C3·#REDIS-LUAAPI-C4 |
| 오류 처리 | redis.call 을 쓰되 오류가 날 상황을 미리 방어한다 — 진입부에서 #KEYS/#ARGV/길이를 검사해 INVALID, TYPE 을 확인해 WRONG_TYPE 을 상태 문자열로 반환한다. redis.pcall 과 redis.error_reply 는 as-built 가 쓰지 않는다 |
#REDIS-LUAAPI-C1(call 은 오류를 그대로 올려 스크립트 중단) + as-built .lua 3개 전부 이 패턴 |
as-built 가 pcall/error_reply 대신 방어적 상태 반환을 택한 결과: 예상 가능한 실패(잘못된 arity·타입 불일치·소유자 불일치)는 오류가 아니라 계약된 상태값으로 나오고, redis.call 이 실제로 오류를 올리는 경우는 예상 못 한 결함뿐이다. 즉 상태값 = 정상 흐름, 예외 = 결함이라는 경계가 script 안에서 이미 갈린다.
4. 배포 모드 — EVAL script 기본, Functions opt-in 게이트
Trace: D6 ←
redis-functions-library-deployment.md#REDIS-FN-C1(7.0+)·#REDIS-FN-C2(영속·복제 vs 휘발성 캐시)·#REDIS-FN-C6(cluster 전 노드 수동 적재)·#REDIS-FN-C4(Functions 도 동일 블로킹) /#REDIS-EVAL-C2(캐시 휘발)
| opt-in 조건 | 판정 | 근거 |
|---|---|---|
| ① 대상 Redis 7.0+ 확정 | 충족 — program-set.json 이 minimumRedisVersion: "7.2" 를 선언한다 |
#REDIS-FN-C1 + as-built manifest |
| ② NOSCRIPT 재적재 부담이 실제 문제로 관측됨 | 미관측 (구현 전) | #REDIS-FN-C2, #REDIS-EVAL-C2 |
| ③ cluster 전 노드 수동 적재 운영을 수용 | 미결정 | #REDIS-FN-C6 |
→ 3조건 모두 충족 전까지 EVAL script 가 기본값이다. Functions 로 전환하면 SSOT 가 library 파일(#!lua name=…)로 이동하고(#REDIS-FN-C3) §1 의 SHA1 기반 버전 식별이 성립하지 않으므로, 전환은 D1 재검토를 동반한다.
5. unsafe recipe ↔ 안전한 대체 쌍 카탈로그
Trace: D7 ←
redis-incr-rate-limiter-pattern.md#REDIS-INCR-C4(공식이 두 변형에 서로 다른 대체를 권고)·#REDIS-INCR-C2(race 명시)·#REDIS-INCR-C3(EXPIRE 유실 → key leak)
- 이 표의 행 자체가 계약이다. helper 를 추가할 때 unsafe 쌍과 경합 test 없이 등재할 수 없다.
| unsafe recipe | 분기 | 만드는 경합 | 안전한 대체 | 근거 |
|---|---|---|---|---|
고정 타임스탬프 키 카운터 — INCR 후 EXPIRE |
없음 | INCR 은 성공했는데 EXPIRE 가 유실될 수 있음 |
MULTI/EXEC 로 충분 — program 을 만들지 않는다 |
#REDIS-INCR-C4(a) |
단일 카운터 — GET → 판단 → INCR → 조건부 EXPIRE |
있음(IF value == 1) |
공식이 "race condition" 을 명시. EXPIRE 미수행 시 key 가 TTL 없이 leak |
atomic program 필수 (Lua/EVAL) | #REDIS-INCR-C2·#REDIS-INCR-C3·#REDIS-INCR-C4(b) |
두 변형을 하나의 "INCR+EXPIRE" 항목으로 뭉치면 #REDIS-INCR-C4 의 구분이 사라진다 — 별개 행으로 유지한다.
6. 테스트 계약 — 경합 증명 2층
Trace: D9 ←
#REDIS-EVAL-C1(원자성은 서버가 보장)·test-taxonomy-testcontainers-official.md#TC-OFFICIAL-C3(mock/in-memory 없이 실제 서비스)·#TC-OFFICIAL-C5(in-memory 는 기능 결여)
- 선행 계약 (다른 branch owner, 재진술 아님): focused(unit/contract/architecture) 레벨의 Testcontainers 금지는 raw/branch-notes/feature-test-taxonomy-fixture-contract 가 소유하며 ca-tmpl 에 ArchUnit 으로 실제 강제돼 있다 —
TestTaxonomyArchitectureTest.contractLevelTestsHaveNoTestcontainersDependency/architectureLevelTestsHaveNoTestcontainersDependency+ 비공허성 증명banRuleFiresOnTestcontainersUsage(등급actually-implemented,src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/TestTaxonomyArchitectureTest.java).- UNSUPPORTED_IMPL_DECISION: 반복 횟수·스레드 수. 어떤 공식 문서도 수치를 권고하지 않는다. trade-off: barrier 로 순서를 강제하면 확률적 반복에 의존하지 않으므로 반복 수를 작게 유지한다 —
flaky-quarantine.yaml의 14일 sunset 압력을 피하는 방향.
| 층 | 무엇을 증명 | 어떻게 | 왜 이 층인가 |
|---|---|---|---|
| focused (unit/contract) | unsafe recipe 의 race 가 실제로 발생함 | fake RedisClient + latch/barrier 로 read 와 write 사이 인터리빙을 결정론적으로 강제 |
race 는 클라이언트 측 순서 문제라 서버 없이 재현 가능. 실제 네트워크 금지 규칙 준수 |
| focused (unit/contract) | atomic 대체에 재현 가능한 client-side window 가 없음 | 성공 경로에서 round-trip 이 정확히 1회임을 spy 로 assert | 구조적 회귀 방지 — 나중에 누가 보조 GET 을 끼워 2-round-trip 으로 되돌리는 것을 잡는다 |
| integration | atomic 대체의 실제 원자성 + NOSCRIPT 재적재 | 실제 Redis(Testcontainers) — 동시 호출 후 불변식 유지 확인, SCRIPT FLUSH 후 재실행 |
원자성은 서버가 보장하는 성질이라 fake 로 증명 불가(#REDIS-EVAL-C1). in-memory 대체는 기능 결여 위험(#TC-OFFICIAL-C5) |
내부 선례(등급 actually-implemented): latch 기반 결정론적 동시성 test 는 이 repo 에 이미 있다 — src/adapter/outbound/persistence-jpa/.../lock/LockRegistryDistributedLockAdapterTest.java (CountDownLatch 사용).
환경 게이트: Redis 를 요구하는 test 는 @EnabledIfRedisCacheEnabled(APP_CACHE_REDIS_ENABLED=true 일 때만 실행, 아니면 SKIPPED) 로 감싼다 — 등급 actually-implemented, src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/contract/support/conditional/EnabledIfRedisCacheEnabled.java.
7. typed facade 와 소비 경계
Trace: D8 ←
#SDR-SCRIPT-C3(execute 시그니처)·#SDR-SCRIPT-C4(반환 타입 제약) + 소비 branch 4건이 명시적으로 "Lua program 실행 기반 — #062 소유" 로 위임
- UNSUPPORTED_IMPL_DECISION: facade 타입·메서드 명명. 공식 근거 없음. trade-off: 능력 이름이 들어가면 D8 이 깨지므로 연산 서술 이름만 쓴다.
| 규칙 | planned 동작 |
|---|---|
| 노출 | program 식별자 + KEYS + ARGV + 반환 타입. 그 이상 없음 |
| 비노출 | rate limit 판정·lock fencing·idempotency claim 결과 등 능력 의미 — 각 소비 branch 소유 |
| 반환 | #SDR-SCRIPT-C4 의 4종을 넘는 복합 구조가 필요하면 program 이 직렬화 형식을 정하고 facade 가 파싱. 그 형식 결정은 근거가 없다(UNSUPPORTED_IMPL_DECISION) |
| 소비자 | raw/branch-notes/feature-edge-rate-limit-policy-registry-contract · raw/branch-notes/feature-idempotency-ownership-protocol-contract · raw/branch-notes/feature-fenced-lock-coordination-contract · raw/branch-notes/feature-cache-strategy-registry-contract |
8. 실패 등급과 error code
Trace: D10 ←
#REDIS-LUAAPI-C1(call은 예외를 그대로 올려 중단 /pcall은 error 테이블 반환)·#REDIS-LUAAPI-C2(error_reply/status_reply가 typed 반환 수단). 등급 분리 규칙 자체의 owner 는 hub §11 매트릭스.
- 신규 제안임을 명시 —
docs/registries/error-codes.yaml에 script/program 관련 code 가 하나도 없다(grep 확인). 아래는 기존 값이 아니라 제안이며 registry-governance 절차를 거쳐야 한다.
등급 판별은 as-built 예외 타입으로 결정론적으로 갈린다 — 추론이 아니라 타입 매칭이다:
| as-built 신호 | 의미 | 등급 | 제안 category | 근거 |
|---|---|---|---|---|
RedisCommandFailureException(kind=UNAVAILABLE) |
연결 불가 | backend 실패 → 선언된 경우 degrade 가능 | TRANSIENT_DEPENDENCY |
기존 CACHE_UNAVAILABLE(503, retryable) 선례 |
RedisCommandFailureException(kind=OVERLOADED) |
admission 거부(in-flight 상한) | backend 실패 | TRANSIENT_DEPENDENCY |
RedisCommandAdmission — 단 이 상한의 owner 는 #061 |
RedisProgramCompatibilityException |
반환 상태가 descriptor.statuses() 밖 |
program 결함 → fail closed | INTERNAL |
hub §11 "codec·프로그래밍 결함은 fail closed" |
IllegalArgumentException (arity·바이트 상한·카탈로그 소유권) |
호출 계약 위반 | program 결함 → fail closed | INTERNAL |
RedisLuaProgramExecutor.validate() |
계약된 상태 문자열 (INVALID·WRONG_TYPE·NOT_OWNER 등) |
실패가 아니다 — 정상 반환 | 호출자가 의미를 해석 | — | §3 오류 처리 규칙 |
RedisCommandFailureException.certainty() 는 NOT_APPLIED / INDETERMINATE 를 구분한다 — 재시도 안전성 판단에 필요하다. INDETERMINATE(적용 여부 불명)를 무조건 재시도하면 program 이 원자적이어도 중복 적용이 생길 수 있다. 이 축의 owner 는 #061(전송 계층)이며 본 branch 는 소비한다.
Category 는 shared/error/Category.java 의 10종 밖 값을 만들 수 없다 (VALIDATION·AUTH·AUTHZ·NOT_FOUND·CONFLICT·RATE_LIMIT·TRANSIENT_DEPENDENCY·PERMANENT_DEPENDENCY·DATA_INTEGRITY·INTERNAL, 등급 actually-implemented).
redis.error_reply 는 as-built 가 쓰지 않는다 — §3 의 방어적 상태 반환이 그 역할을 대신한다. #REDIS-LUAAPI-C2 는 typed 오류 반환의 공식 수단을 증명할 뿐 as-built 가 그것을 쓴다는 근거가 아니다.
9. program 실행 관측 — metric + span
Trace: D11 ← raw/branch-notes/feature-metrics-alerting-contract
D2(Micrometer dot.case + unit suffix 명명)·D8(high-cardinality tag 금지 + bounded whitelist) — 그 branch 가 owner 이고 본 §는 소비자다. 선례:metrics.yaml의lock.acquisition(ownerfeature-distributed-lock-contract) — "key 는 tag 로 넣지 않는다(무한 cardinality)".
- UNSUPPORTED_IMPL_DECISION: metric 이름과
outcome어휘. 공식 근거 없음. trade-off:lock.acquisition의 형태(counter +outcome단일 bounded tag)를 그대로 따라 registry 스타일 일관성을 택했다.program_id를 tag 로 추가한 것은 카탈로그가 닫힌 enum(현재 3개)이라 cardinality 가 구조적으로 유한하기 때문이다.- UNSUPPORTED_IMPL_DECISION: cardinality 상한 수치. metrics-alerting
D8자체가 정량 상한(≤200/≤50)을 "공식 spec 없는 운영 가정" 으로 라벨했으므로 본 §도 같은 성격이다.
| 항목 | planned 명세 | 근거 |
|---|---|---|
| metric | redis.program.executions.total (counter, unit total) |
metrics-alerting D2 명명 규약 |
tag program_id |
값 = RedisProgramId.externalId(). cardinality 는 닫힌 enum 크기로 bounded (현재 3, 상한 50 제안) |
as-built 카탈로그가 EnumMap 닫힌 집합 |
tag outcome |
success · noscript_reload · defect · backend_failure (4종 고정) |
§8 의 예외 타입 매핑과 1:1 |
| 금지 tag | key·KEYS 값·ARGV 값·상태 문자열 원문 — 사용자 입력 유래이거나 무한 cardinality | metrics-alerting D8, lock.acquisition 선례 |
| alert | defect rate 지속 > 0 이면 P2 — program 결함은 조용히 넘어가면 안 된다 |
hub §11 fail-closed 원칙 |
| trace span | program 실행 1회 = span 1개. attribute 는 program_id·outcome 만. key·ARGV 를 span attribute 로 넣지 않는다 |
hub §36.2 "bounded cardinality metric 과 trace span" |
noscript_reload 의 쓰임 |
이 값이 꾸준히 0 이 아니면 캐시가 반복 소실된다는 신호 — D2 의 EVAL 폴백이 정상 경로가 되어버린 상태를 드러낸다 | #REDIS-EVAL-C2(캐시 휘발성) |
registry 등재: 위 metric 은 docs/registries/metrics.yaml 에 신규 제안 이다(현재 script/program/redis 관련 row 0건 — grep 확인). owner_branch: feature-redis-atomic-program-catalog-contract, compatibility_impact: additive, required_test: contract-verification:metrics-cardinality 로 등재하며 절차는 raw/branch-notes/feature-contract-registry-governance 소관이다.
엣지·실패·의존
- 실패·엣지 경로
- 스크립트 캐시 소실 — 재시작·failover·
SCRIPT FLUSH로 언제든 발생. EVALSHA 가NOSCRIPT를 받는다 → 재적재 후 재시도(§구현 가이드 2). 애플리케이션 책임임이 공식 명시(#REDIS-EVAL-C2·#REDIS-EVAL-C3) - pipeline 안의 NOSCRIPT — 처리 불가. pipeline 경로는 애초에 plain EVAL 로 되돌린다(
#REDIS-EVAL-C4, D3) - 폭주 program 이 쓰기를 이미 수행함 —
SCRIPT KILL로 죽일 수 없다(#REDIS-EVAL-C6). 5초(busy-reply-threshold) 초과 후 서버는 다른 클라이언트에BUSY로 응답하고, 이 상태에서 남은 수단은SHUTDOWN NOSAVE— 인스턴스 강제 정지뿐이다(#REDIS-PROG-C1·#REDIS-PROG-C3·#REDIS-PROG-C4). 즉 이 엣지의 최종 비용은 가용성 사고다(§구현 가이드 3) - 미선언 key 접근 — Redis 가 막아주지 않는다(
#REDIS-EVAL-C5의 does-not-prove: 거부 동작·에러 코드가 공식에 없음). 우리가 정적 검사로 잡지 못하면 조용히 통과한다 - program 결함 — fail closed. cache miss 로 위장하지 않는다(hub §11, D10)
- descriptor ↔
.luadrift —keyCount/resultType은 수기 선언이라 본문 변경 시 자동 갱신되지 않는다. golden vector 가 이 drift 를 잡는 유일한 장치(§구현 가이드 1) - 경합 test 자체의 flakiness — 확률적 재현에 의존하면
flaky-quarantine.yaml의 14일 sunset 압력을 받는다. barrier 로 순서를 강제해 회피(§구현 가이드 6)
- 스크립트 캐시 소실 — 재시작·failover·
- 다른 계약 의존 (대상 브랜치 + 그 Decision ID)
- raw/branch-notes/feature-redis-runtime-role-isolation-contract (
WI-…-061) — 그 branch 가 이미 D1~D22 를 보유하며(스캐폴딩 아님) 다음을 소유한다:D5(key builder)·D6(digest)·D8(key/payload/policy 3 version 축 분리)·D11(envelope)·D10(byte[]codec) 은 program descriptor 의 입력이고,D16~D18(Lettuce ClientOptions·timeout·queue) 은 본 branch 의 실행 경로가 올라타는 전송 계층이다.D2(SDK 선택)가 raw Lettuce 로 확정된 것이 본 branch D1·D2 의 전제다 - raw/branch-notes/feature-test-taxonomy-fixture-contract
D3(focused 레벨 Testcontainers 금지, ArchUnit 강제)·D4(CI 시간 budget) — D9 의 2층 분리는 이 계약의 결과이지 본 branch 의 재량이 아니다(재진술 아님, 포인터만) - 위임을 선언한 소비 branch (2건, 확인됨) — raw/branch-notes/feature-edge-rate-limit-policy-registry-contract (§제외 범위 "Lua program 실행 기반 — #062 소유") · raw/branch-notes/feature-fenced-lock-coordination-contract (§제외 범위 "Redis 연결·스크립트 기반 — #061·#062 소유")
- 위임 미선언 소비 branch (2건) — raw/branch-notes/feature-idempotency-ownership-protocol-contract · raw/branch-notes/feature-cache-strategy-registry-contract 는 §제외 범위에 program 위임 문구가 없다(grep 0건). hub §25 Owner Map 은 이 둘을 consumer 로 적지만 노트 쪽 선언이 비어 있어 양방향 정합이 깨져 있다 —
/sync로 맞춰야 하며, 그 전까지 "4건 모두 위임" 이라고 주장할 수 없다 - raw/branch-notes/feature-operational-error-observability-foundation — D10 의 신규 error code 는
error-codes.yaml스키마와Categoryenum(10종)을 따른다. 해당 branch 의 category enum owner D-row 를 pin 해야 하나 아직 번호 미확인(Should-fix) - raw/branch-notes/feature-capability-provider-selection-contract (
WI-…-060, 선행 필수) — D6 의 배포 모드(EVAL script ↔ Functions) 는 hub §25 기준 capability 활성화·provider 선택의 일종이며, 그 판정은 "typed descriptor 로만" 하고 bean 이름·classpath 존재를 근거로 삼지 않는다. 본 branch 는 3조건 opt-in 논리만 소유하고, 그 스위치가 어떤 typed descriptor 로 노출되는지는 그 branch 소유다 - raw/branch-notes/feature-metrics-alerting-contract
D2(dot.case 명명)·D8(cardinality bounds·금지 tag) — D11 의 metric·span 은 이 계약의 소비자다.D8이 바뀌면 §구현 가이드 9 의 tag 집합이 함께 바뀐다
- raw/branch-notes/feature-redis-runtime-role-isolation-contract (
검증해야 할 주장 / Claims To Verify
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
배포 이미지의 실제 redis_version 이 7.2 이상이다 |
program-set.json 이 minimumRedisVersion: "7.2" 를 선언하지만, 그것이 배포 환경에서 실제로 지켜지는지는 별개다. 미달이면 D6 의 조건 ①과 #REDIS-EVAL-C7(effects replication 유일 모드)이 함께 깨진다 |
배포 대상에서 INFO server 의 redis_version 확인 + startup 시 최소 버전 검증을 둘지 결정 |
needs-confirmation |
| as-built 의 EVAL 폴백이 Redis 공식 절차와 동등하다 | as-built 는 NOSCRIPT 시 EVAL 직접 실행으로 복구하고, #REDIS-EVAL-C3 은 "SCRIPT LOAD 후 EVALSHA 재시도" 를 서술한다 — 서로 다른 경로다. EVAL 이 캐시에 넣는 것은 부수효과이며 코드가 관측하지 않는다 |
실제 Redis lane 에서 SCRIPT FLUSH → 실행 → SCRIPT EXISTS <sha> 로 재적재 여부 확인 |
needs-confirmation |
| D3(pipeline plain EVAL)이 공허하지 않다 | as-built RedisBinaryCommands 는 get/set/delete/evalSha/eval 5개뿐이고 pipeline 표면이 없다. 즉 D3 은 현재 도달 불가능한 조건 위의 결정이다 |
pipeline API 를 실제로 추가할지 결정 → 추가하지 않으면 D3 을 "향후 확장 시 제약" 으로 재분류 | planned |
| 정적 검사가 bounded execution 위반을 실효적으로 잡는다 | 정규식 텍스트 검사는 문자열 조립('S'..'MEMBERS')으로 우회된다. "완전한 강제" 라는 인상만 주고 실제로는 부분 강제일 수 있다 |
의도적 우회 fixture 를 넣어 검사기가 놓치는지 확인(비공허성 증명). 놓치면 그 한계를 문서에 명시 | planned |
| barrier 로 강제한 인터리빙이 매 실행 race 를 재현한다 | barrier 는 read/write 의 논리적 순서만 강제하고 서버가 명령을 수신하는 wire-order 까지 통제하지 못한다 | 동일 test 를 CI 에서 연속 반복 실행해 재현율 측정. 100% 가 아니면 quarantine 대상이 된다 | planned |
| atomic 대체가 실제로 경합을 없앤다 | 원자성은 서버가 보장하는 성질이라 fake 로 증명 불가(#REDIS-EVAL-C1). in-memory 대체도 기능 결여 위험(#TC-OFFICIAL-C5) |
실제 Redis 통합 test — 동시 호출 후 불변식 유지 확인. 이는 통계적(위반 미관측) 증명이지 결정론적 증명이 아님을 명시 | planned |
| golden vector 가 descriptor drift 를 잡는다 | keyCount·resultType 은 수기 선언이고 .lua 본문 변경 시 자동 갱신되지 않는다 |
descriptor 는 그대로 두고 .lua 의 KEYS 사용 개수만 바꾼 fixture 로 test 가 실패하는지 확인 |
planned |
| 신규 error code 가 registry 절차를 통과한다 | error-codes.yaml 에 script/program code 가 하나도 없어 전부 신규 제안이다. Category 는 10종 밖으로 못 나간다 |
registry-governance 절차 + contract-verification test 로 매핑 검증 |
planned |
maxmemory 초과 상태에서 program 이 abort 된다 |
D4 는 bounded 시간만 다루고 bounded 메모리는 다루지 않는다. "maxmemory 초과 시 추가 메모리를 쓰는 첫 write 가 스크립트를 abort 시킨다"는 서술은 수집한 두 자료(redis-programmability-max-execution-time·redis-key-eviction-maxmemory-policy) 어디에도 없어 인과관계가 미증명이다 |
공식 근거를 별도 수집하거나 실제 Redis 에서 maxmemory 근접 상태로 write program 실행해 재현. 확인되면 D4 에 메모리 축을 추가하고, maxmemory-policy 값 선택은 #061(배포 설정 소유)로 위임 |
needs-confirmation |
| 본 branch 가 소유할 관심사가 sibling branch 결정과 겹치지 않는다 | 소비 branch 4건 중 2건만 위임을 선언했고(edge-rate-limit·fenced-lock), idempotency·cache-strategy 는 아직 scaffolding 이라 선언이 없다 | 그 2건 /branch-spec 후 /sync 실행 — owner 중복 검출 |
needs-confirmation |
Audit & Findings
/branch-spec2026-07-28 조사에서 발견한 상위 문서의 사실 오류. 대상은 hub / 설계 문서이므로 본 branch 는 정합 권고만 내고 자동 수정하지 않는다.
| Finding ID | 대상 | 현재 서술 | 조사 결과 | 권고 | 처리 |
|---|---|---|---|---|---|
GROUND_TRUTH_PATH_DRIFT |
.claude/commands/branch-spec.md §참조 + llm-wiki 다수 문서 |
ca-tmpl ground truth 경로를 /home/donghyeon/workspace/ca-tmpl/ 로 지정 |
그 경로는 낡은 clone 이다. 실제 최신 저장소는 /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/ 이며, cache-redis 가 47개 src 파일을 보유하고 RedisProgramCatalog·RedisProgramDescriptor·RedisProgramExecutor·RedisLuaProgramExecutor·RedisProgramId·LettuceRedisRuntime·RedisKeyBuilder·RedisCacheEnvelopeCodec + .lua 3개 + program-set.json 이 모두 존재한다. 낡은 clone 에는 14개뿐이라 "코드 없음" 으로 오판하기 쉽다 |
명령 문서의 ground truth 경로를 정정. 같은 사실을 raw/branch-notes/feature-redis-runtime-role-isolation-contract §Audit A1 과 설계 §13 D2 가 독립적으로 기록했다 | open — harness/사용자 소유 |
SELF_CORRECTION (본 세션) |
본 노트 초판의 CODE_CLAIM_DRIFT finding |
초판은 "4개 클래스가 어디에도 없고 commit b3add01 도 없으므로 hub §35 를 [ ](미시작)로 강등하라" 고 권고했다 |
그 권고는 틀렸다. 낡은 clone(/home/donghyeon/workspace/ca-tmpl)만 조회한 결과이며, 실제 저장소에는 4개 클래스가 모두 존재한다. hub §35 의 [*](부분 구현) 표기가 맞다. 설계 §14.2 의 actually-implemented 등급도 유효하다(단 파일 수 "43개" 는 현재 47개로 증가) |
hub·설계 문서에 대한 강등 권고를 철회한다. 남는 정정 대상은 파일 수 숫자와 commit 해시(b3add01 은 낡은 clone 기준 미존재)뿐이며 경미하다 |
partially-resolved 2026-07-28 — D-row(D1·D2·D4·D8·D9·D10·D11)·§구현 가이드 3·8·9·§TODO·§결정 사항·§진행 중 메모·§Claims To Verify 는 as-built 로 재작성했다. §구현 가이드 1·2·6·7 은 아직 초판 전제가 남아 있다 — §구현 가이드 preamble 의 충돌 표 참조 |
SPRING_DATA_REDIS_PREMISE_RESOLVED |
본 노트 초판 D1·D2 | 초판은 D1·D2 를 Spring Data Redis 의 RedisScript/DefaultRedisScript/ScriptExecutor 전제 위에 세웠다 |
as-built 는 io.lettuce:lettuce-core 단독이고 spring-data-redis 의존성이 없다(cache-redis/build.gradle 확인). EVALSHA→NOSCRIPT→EVAL 폴백은 RedisLuaProgramExecutor 가 직접 구현한다 |
D1·D2 를 as-built 기준으로 재작성했다. raw/branch-notes/feature-redis-runtime-role-isolation-contract §Audit A2 가 이 충돌을 SPRING_DATA_REDIS_PREMISE_CONFLICT 로 열어두고 본 노트를 가리키는데, 본 노트 쪽은 해소됐으므로 그 항목과 §구현 가이드 4 의 위험 서술이 낡았다 — 그 branch 소유라 여기서 고치지 않고 알림만 남긴다 |
resolved (본 노트 측) / #061 측 갱신 필요 |
NO_VERSION_PIN (철회) |
본 branch D6 의 opt-in 조건 ① | 초판은 "ca-tmpl 에 Redis 버전이 고정된 곳이 없어 7.0+ 판정 불가" 라고 단정했다 | 틀렸다 — 낡은 clone 기준이었다. 실제 저장소의 redis/program-set.json:3 에 "minimumRedisVersion": "7.2" 가 있다. 즉 D6 의 조건 ①(7.0+)은 이미 충족이고, #REDIS-EVAL-C7(effects replication 유일 모드, 7.0+ 전제)도 확정으로 승격된다 |
D6 게이트를 ②③ 로 좁혔다. 남는 확인은 "배포 이미지의 실제 redis_version 이 7.2 이상인가" 라는 운영 확인뿐 |
resolved 2026-07-28 |
DANGLING_DESIGN_REF (철회) |
본 노트 §근거의 "프로젝트 내부 설계 참조" | 초판은 "두 설계 문서가 부재하므로 인용 근거에서 제외" 라고 권고했다 | 틀렸다 — 낡은 clone 기준이었다. 실제 저장소 docs/superpowers/specs/ 에 2026-07-26-production-capability-platform-design.md 와 2026-07-26-redis-production-capability-design.md(as-built 를 낳은 6,700줄 설계)가 둘 다 실재한다 |
두 문서를 internal-design-doc 등급으로 §근거에 유지한다. 다만 본 노트의 현재 결정은 아직 이 문서들을 읽고 쓴 것이 아니다 — 다음 세션에서 정독해 §1 golden vector·§9 metric·§3 denylist·BUSY 관측의 UNSUPPORTED_IMPL_DECISION 라벨을 항목별로 재판정해야 한다 |
open — 정독 미완 |
STALE_CLONE_ROOT_CAUSE |
본 세션의 조사 방법 | 위 SELF_CORRECTION·NO_VERSION_PIN·DANGLING_DESIGN_REF 3건이 모두 같은 원인에서 나왔다 |
/branch-spec 명령 문서가 지정한 /home/donghyeon/workspace/ca-tmpl 를 ground truth 로 삼은 결과, "코드 없음·버전 없음·설계문서 없음" 이라는 3중 오판이 연쇄했다 |
명령 문서의 ground truth 경로를 정정하기 전까지 이 branch 계열의 조사는 반드시 desktop-server-git/clean-architecture-backend-template/ 를 쓴다 |
open — harness/사용자 소유 |
EVALSHA_COUNTEREXAMPLE |
본 branch D2 (EVALSHA 우선) | 완료 조건이 "NOSCRIPT 재적재 test" 를 요구 → EVALSHA 우선을 전제 | 조사 중 LINE engineering 이 Lua 를 쓰되 EVALSHA 를 의도적으로 거부한 사례가 확인됐다(노드·키 관리 비용 vs 대역폭 절감을 비교한 결과). 다만 아직 raw 로 archive 하지 않아 근거로 인용하지 않았다 | 클러스터 노드 수·호출 빈도가 확정되면 EVALSHA 의 실익을 재평가. 필요 시 해당 블로그를 raw/company-tech-blogs/ 로 수집 후 D2 재검토 |
open — 근거 미수집, 결정 변경 없음 |
Coverage / 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
coverage-auditor생성물 — 손으로 유지하지 않는다. 2026-07-28 loop 2/2 판정: Covered (Blocking 0 / Should-fix 0 / Advisory 3). governing doc = raw/project-notes/ca-skeleton-operational-contract (§8.0 WI-062 · §25 Owner Map · §36.2 capability card · §11 실패 매트릭스). 코드 대조는desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/.
| 관심사 | 상태 | owner | 심각도 | 근거 |
|---|---|---|---|---|
| WI-062 ① program descriptor | covered-here | — | — | D1 · RedisProgramDescriptor.java 실재 |
| WI-062 ② golden vector | covered-here | — | — | D1 · RedisProgramCatalogTest.loadsEveryFoundationProgramWithAnExactDigestAndBoundedSignature |
| WI-062 ③ NOSCRIPT 재적재 test | covered-here | — | — | D2 · RedisLuaProgramExecutorTest.fallsBackToEvalOnlyWhenEvalShaReportsNoScript |
| WI-062 ④ unsafe recipe 경합 재현 test | covered-here (결정) / 코드 planned |
— | — | D9 + §구현 가이드 6. 모듈에 latch/thread 코드 0건 — 노트의 "3 구현 / 1 미구현" 자기평가가 코드와 일치함이 검증됨 |
| §36.2 bounded cardinality metric · trace span | covered-here | — | — | D11 + §구현 가이드 9 (loop 1 의 🔴 Blocking 해소) |
| §36.2 나머지 12항목 (provider ID·readiness·비용모델·rolling upgrade·runbook 등) | delegated | feature-capability-provider-selection-contract |
— | hub §36 헤더가 그 branch 를 card owner 로 지정. 본 branch 는 Tier T(기술 런타임)이므로 card 전체를 채울 의무가 없다 |
| §11 실패 정책 매트릭스 정합 | covered-here (consumer) | 규칙 owner: feature-capability-provider-selection-contract |
— | D10 · Category.java 10종 enum 과 노트 열거가 100% 일치 확인 |
| 배포 모드 typed descriptor 노출 | delegated + linked | feature-capability-provider-selection-contract |
— | §엣지·실패·의존 |
| cluster / hash-slot 라우팅 | delegated + linked | feature-redis-runtime-role-isolation-contract |
— | §엣지·실패·의존 (#061 D5·D6·D8·D10·D11·D16~D18 pin) |
| idempotency branch 의 위임 미선언 | delegated (consumer 쪽 미선언) | feature-idempotency-ownership-protocol-contract |
⚪ Advisory | 그 노트가 아직 scaffolding(D-row 0건). 본 branch 는 이미 gap 을 서술·귀속했으므로 UNLINKED_DELEGATION 아님 — 그쪽 /branch-spec 으로 해소 |
| cache-strategy branch 의 위임 미선언 | delegated (consumer 쪽 미선언) | feature-cache-strategy-registry-contract |
⚪ Advisory | 위와 동일 |
마주친 문제
아직 없음.
묶음 (이 branch에서 파생된 자료)
관련 일일 노트
해당 없음.
완료 후 정리
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- wiki 추출 대상 (verified만,
wiki/projects/로만 추출): - 추출하지 않을 항목 (planned / documented-only / abandoned):