Files
llm-wiki/raw/branch-notes/feature-capability-provider-selection-contract.md

65 KiB
Raw Permalink Blame History

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-capability-provider-selection-contract branch-note raw feature-capability-provider-selection-contract
ca-skeleton
raw/project-notes/ca-skeleton-operational-contract
branch
ca-skeleton
capability-platform
provider-selection
readiness
2026-07-28 in-progress BR-CA-SKELETON-OPERATIONAL-CONTRACT-060 project-work-item ca-skeleton-operational-contract WI-CA-SKELETON-OPERATIONAL-CONTRACT-060
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-CAPABILITY-SELECTION-001@1
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-CAPABILITY-READINESS-001@1
1

branch: feature-capability-provider-selection-contract

Layer: raw/branch-notes/ — 단일 브랜치의 TODO·결정·진행 기록. 머지/종료 후 verified 결과는 /ingestwiki/projects/에 추출. 원본은 raw에 영구 보관. status_label: in-progress | review | merged | abandoned 스캐폴딩 상태 — 결정(D-row)·구현 가이드는 비어 있다. /branch-spec feature-capability-provider-selection-contract 로 채운다.

부모 (필수)

분해 근거: docs/superpowers/specs/2026-07-28-ca-skeleton-production-capability-feature-decomposition-design.md §4.2 — 플랫폼 (Tier P). 본 branch 는 project §8.0 WI-CA-SKELETON-OPERATIONAL-CONTRACT-060 의 실행 단위다.

형제 branch (같은 부모의 다른 자식 — 인접 영역):

브랜치 계약 패킷

project Work Item 에서 내려온 실행 계약의 snapshot. 여기에는 pinned pointer + 1줄 요약 + branch 적용점만 쓰고 상세를 복제하지 않는다.

  • 생성 시 프로젝트 개정: 1
  • 패킷 스키마: contract_packet: 1
  • 완료 조건: capability descriptor·provider 선택·readiness 등급 검증 test 가 통과하고 bean 이름 기반 판정이 제거된다

상속한 프로젝트 결정

Decision Ref Project Summary Branch Application Source
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-CAPABILITY-SELECTION-001@1 production capability는 available by default이고 inactive by default이며 활성화는 typed descriptor 기반 provider 선택으로만 이뤄진다 /branch-spec 에서 적용 경계 작성 raw/project-notes/ca-skeleton-operational-contract
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-CAPABILITY-READINESS-001@1 모든 capability는 R0~R3 readiness 등급과 capability card를 가지며 R0 seam을 R2 provider로 표기하지 않는다 /branch-spec 에서 적용 경계 작성 raw/project-notes/ca-skeleton-operational-contract

브랜치 지역 결정

상세 근거와 선택 조건은 아래 ## 결정-근거 매핑의 동일 D-row 가 소유한다. 여기에는 요약과 관계만 둔다.

Decision ID Decision Relation Supporting Claims Status
D1 런타임 능력 어휘를 use-case capability 와 분리 local internal-code-fact proposed
D2 활성화 SSOT 는 provider/mode/boolean 중 한 축만 local spring-boot-condition-annotations.md#SB-COND-C1 proposed
D3 활성화 property 는 단일 prefix + APP_* env registry 주입 refines DEC-CA-SKELETON-OPERATIONAL-CONTRACT-CAPABILITY-SELECTION-001@1 config-spring-boot-externalized-configuration.md#SPRING-EXTCONFIG-C7 proposed
D4 production capability 에 matchIfMissing = true 금지 (프레임워크 기본값 유지) local spring-boot-conditional-on-property-javadoc.md#SB-COND-JD-C1 proposed
D5 활성화·안전 판정은 typed descriptor 로 (bean 이름 아님) local spring-boot-condition-annotations.md#SB-COND-C3 proposed
D6 토폴로지 검증은 refresh 완료 전, 실패 시 기동 거부 local spring-boot-application-availability-startup.md#SB-AVAIL-C4 proposed
D7 readiness 반영은 descriptor 의 readinessImpact 로 판정 local runtime-health-spring-actuator-groups.md#SB-HEALTH-C12 proposed
D8 모든 capability 에 R0~R3 등급 + 등급별 증거 refines DEC-CA-SKELETON-OPERATIONAL-CONTRACT-CAPABILITY-READINESS-001@1 UNSUPPORTED_DECISION proposed
D9 capability card 에 보증·비보증 + unsafe recipe ↔ 대체 + 경합 test 이름 refines DEC-CA-SKELETON-OPERATIONAL-CONTRACT-CAPABILITY-READINESS-001@1 UNSUPPORTED_DECISION proposed
D10 startup 실패는 기존 registry code 재사용, 신규 code 금지 local internal-code-fact proposed
D11 descriptor registry 는 governance 스키마 준수 + 별도 파일 local sibling raw/branch-notes/feature-contract-registry-governance D4 proposed
D12 project note §11 실패 정책 매트릭스 채택 + failurePolicy 맵 파생 local project note §11 (owner 선언) proposed
D13 비활성 capability 는 연결·워커·스키마·health contributor 미생성 local spring-boot-condition-annotations.md#SB-COND-C1 proposed
D14 legacy 키 병행 시 SSOT 불일치는 startup 오류 local project note §36.3 규범 3 proposed
D15 capability card 필수 항목은 §36.2 원문 상속, narrow 금지 refines DEC-CA-SKELETON-OPERATIONAL-CONTRACT-CAPABILITY-READINESS-001@1 project note §36.2 proposed

선언한 예외

Override ID Overrides Reason Approval Status

가져온 프로젝트 계약

Ref Owner 요약 Branch 적용

목표

  • WI-CA-SKELETON-OPERATIONAL-CONTRACT-060 의 완료 조건을 구현한다: capability descriptor·provider 선택·readiness 등급 검증 test 가 통과하고 bean 이름 기반 판정이 제거된다

  • 이슈:

  • PR:

범위

포함 범위

  • capability descriptor 필수 필드 (capabilityId·providerId·readiness·guarantee·failurePolicy·multiInstance·requiredRole·readinessImpact·implVersion)
  • 활성화 SSOT — provider: disabled|<id> / dispatch-mode / leaf enabled 의 적용 조건
  • readiness 등급 R0~R3 정의와 승급 증거
  • capability card 필수 항목 (unsafe recipe ↔ 안전한 대체 포함)
  • fail-fast 토폴로지 검증 규칙
  • 능력별 실패 정책 매트릭스의 owner

제외 범위

의도적으로 제외한 것. 면접 등에서 "이건 범위에 없었습니다"라고 답할 근거.

  • 개별 capability 의 provider 구현 — 각 Tier T/C branch 소유
  • capability 별 임계값·용량 수치
  • 심층 명세 — 본 노트는 스캐폴딩. D-row 와 §구현 가이드는 /branch-spec 이 채운다.
  • project decision registry 변경 — owner 는 project-note

근거 (필수, 최소 1개+)

/branch-spec feature-capability-provider-selection-contract 단계에서 verbatim 인용과 함께 raw/official-docs/ 로 수집한 뒤 여기서 링크한다.

Source 정당화하는 결정
raw/official-docs/spring-boot-application-availability-startup capability 토폴로지 검증은 startup 에서 fail-fast 로 수행하고 검증 실패는 컨텍스트 기동을 거부한다 / availability state(readiness·liveness)는 런타임 신호이지 startup 검증의 대체물이 아니다 — SB-AVAIL-C1~C5
raw/official-docs/spring-boot-condition-annotations production capability 활성화를 @ConditionalOnProperty 로 게이트할 때 matchIfMissing=true 를 금지하는 결정(SB-COND-C1/C2), 조건 평가 시점·순서 의존성의 한계(SB-COND-C3/C4)
raw/official-docs/config-spring-boot-externalized-configuration capability 활성화 property 를 단일 prefix 로 통일 — relaxed binding(SPRING-EXTCONFIG-C6/C7)·canonical form 권장(C8)·@ConfigurationProperties prefix→bean 명명 규칙(C9)·type-safe binding 이점(C10)·PropertySource 우선순위(C11, OS 환경변수가 config data 파일보다 우선)를 근거로, ca-skeleton.*/app.* 이중 prefix 를 APP_* 환경변수 registry 와 정합되는 단일 prefix 로 통일
raw/official-docs/runtime-health-spring-actuator-groups 활성화된 provider 만 readiness 에 반영 — optional cache 실패는 unready 미유발, session/idempotency/필수 lock 실패는 unready 유발. StatusAggregator 기반 aggregation(SB-HEALTH-C10,C11) + health group include/exclude 메커니즘(SB-HEALTH-C9) + "essential 하지 않은 외부 시스템은 포함하지 말 것" 판단 기준(SB-HEALTH-C12)이 근거. 단 "capability descriptor 로 판정 주체를 통일한다"는 것 자체는 본 자료가 증명하지 않는 ca-skeleton 자체 설계 결정
raw/official-docs/spring-boot-conditional-on-property-javadoc D4("matchIfMissing=true 금지")의 메커니즘 축 승급 근거 — matchIfMissing 의 선언된 기본값은 false 이고, property 가 Environment 에 전혀 없을 때 기본적으로 조건은 match 되지 않는다는 것이 annotation 공식 Javadoc 에 명시됨(SB-COND-JD-C1/C2). 단 "그래서 명시적으로 켜지 않는다"는 정책 자체는 여전히 internal
(추가 수집 대상) 1차 수집 대상: 설계 §6 / §9 · ca-tmpl platform 설계 §6·§7

프로젝트 내부 설계 참조 (등급 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 11개·§구현 가이드 6절·§Audit & Findings 작성 — 등급: documented-only
  • 미결 3건 사용자 확정 — (1) 런타임 능력 어휘, (2) 통일할 property prefix, (3) 등급 승급 자동 검사 방법 — 등급: planned
  • A4 registry 미등록 키 4종 등록 — 등급: planned
  • A3 notification dead config 를 notification-provider-spi 와 협의 — 등급: planned
  • A5 matchIfMissing=true 6곳 개별 판정 — 등급: planned
  • descriptor 도입 + StartupSafetyValidator 판정 입력 교체 — 등급: planned
  • capability descriptor·provider 선택·readiness 등급 검증 test 가 통과하고 bean 이름 기반 판정이 제거된다 — 등급: planned

진행 중 메모

  • 게이트 이력 (2026-07-28): depth 3회 · coverage 2회 실행. coverage 최종 = Covered(Blocking 0 / Should-fix 0 / Advisory 2, 2회차). depth 최종 = Not ready(Blocking 1 / Should-fix 4 / Advisory 3, 3회차) — /branch-spec 루프 천장 2회 도달로 종료. 잔여 finding 은 다음 세션 재개 대상.
  • ⚠️ 최종 게이트 이후 적용된 편집 2건 (게이트 미검증): (1) §구현 가이드 3-1 의 대조축 시점 배정 — depth 3회차 Blocking #1 이 지적한 자기모순(§11 이 런타임 로딩되지 않는데 "startup 거부"라 씀)의 해소, (2) failureModefailurePolicy 잔존 4곳 정정. 둘 다 게이트 재판정을 거치지 않았다.
  • 2026-07-28 /branch-spec 1회차. 외부 공식 문서 5건 수집(전량 self-grep 검증), sibling 3건 대조, ca-tmpl 코드 7개 영역 확인.
  • depth/coverage 게이트 1회차가 Blocking 4건(depth 3 + coverage 1)을 잡아 보강 루프 1회 수행 — D12·D13·D14 신설, descriptor 기여·수집 메커니즘 확정, readiness group 정적 목록 현행(A8) 기록.
  • 5번째 수집(ConditionalOnProperty Javadoc)은 depth Blocking #1 해소용 후속 dispatch. matchIfMissing 기본값 false 확인 → D4 를 통째 UNSUPPORTED_DECISION 에서 "메커니즘 official / 정책 internal" 2축으로 승급.
  • 수집 중 확인된 근거 부재 2건을 결정에 반영: (1) matchIfMissing 금지의 벤더 권고 부재 → D4 UNSUPPORTED_DECISION, (2) default readiness group 의 외부 의존성 정책 verbatim 부재(SB-HEALTH-C8) → §Claims To Verify.
  • 같은 URL 이 이미 보관돼 있던 2건(external config / actuator health)은 새 파일을 만들지 않고 다중 부모로 확장했다 (rules/linking-rules.md §3).

결정 사항

  • 2026-07-28: 런타임 능력 어휘를 use-case capability 와 분리 (D1) / 이유: capabilities.yaml 7 row 와 @UseCaseCapability 가 이미 그 낱말을 점유 / 검토한 대안: 기존 어휘 재정의(7 row + ArchUnit rule cascade) / 근거: internal-code-fact
  • 2026-07-28: 활성화 판정을 bean 이름 presence 에서 typed descriptor 로 이전 (D5) / 이유: 같은 이름의 아무 bean 이나 있으면 안전으로 보고됨 + 조건 평가가 bean definition 순서에 의존 / 검토한 대안: bean 타입 기반 lookup(순서 의존은 동일) / 근거: raw/official-docs/spring-boot-condition-annotations.md#SB-COND-C3
  • 2026-07-28: matchIfMissing = true 금지는 외부 권고가 아니라 내부 정책임을 명시 (D4) / 이유: Spring 공식 문서가 이 attribute 의 부재 시 의미를 설명하지 않음 / 근거: 코드 사실 — gRPC 서버와 filesystem object storage 가 설정 없이 활성

결정-근거 매핑

Supporting Claimsraw/...#Cn 은 verbatim 인용 + self-grep 검증을 마친 claim. internal-code-fact 는 ca-tmpl 코드를 직접 읽어 확인한 사실(경로·행 명시). 외부 공식 근거가 없는 결정은 UNSUPPORTED_DECISION 으로 라벨한다 — 추측으로 채우지 않는다.

근거의 두 축을 섞지 않는다. 공식 문서가 증명하는 것은 메커니즘(Spring 이 어떻게 동작하는가)이고, 정책(그래서 우리는 무엇을 금지하는가)은 대부분 내부 결정이다. 각 행의 Evidence Strength 가 이 경계를 표시한다.

Decision ID Decision 선택 조건 (언제 이 결정 / 언제 대안) Supporting Claims Evidence Strength Open Risk
D1 런타임 능력에는 capability 라는 낱말을 단독으로 쓰지 않는다 — 그 어휘는 이미 use-case repository access 개념이 점유하고 있으므로 런타임 축은 별도 어휘로 분리한다 기존 @UseCaseCapability + capabilities.yaml 을 유지 → 런타임 축에 신규 어휘. 기존 어휘를 런타임 의미로 재정의 → 7개 capability row 와 ArchUnit rule 전체 cascade (선택 안 함) internal-code-fact: src/application-core/src/main/java/dev/caskeleton/application/capability/UseCaseCapability.java(+RepositoryAccess·Idempotency), docs/registries/capabilities.yaml(owner_branch feature-repository-access-permission-contract, 7 row) internal-code-fact 어휘 명칭 자체는 근거 없음 → §구현 가이드 1 에서 UNSUPPORTED_IMPL_DECISION. 명칭 확정 전 다른 branch 가 capability 를 런타임 의미로 쓰기 시작하면 되돌리기 비용 증가
D2 활성화 SSOT 는 축 하나로 고정한다 — provider 축(provider: disabled|<id>), mode 축(dispatch-mode: ...), 또는 축이 없는 leaf 의 boolean 중 정확히 하나만 한 capability 에 적용한다 provider 대안이 둘 이상 → provider 축. 동작 방식만 갈리고 provider 는 하나 → mode 축. 켜고 끄는 것 외에 선택지가 없음 → boolean. 두 축을 동시에 두는 구성은 금지 SB-COND-C1(raw/official-docs/spring-boot-condition-annotations.md#SB-COND-C1 — property 존재·값 기준 매칭 메커니즘). 현행 4종 혼용: internal-code-fact — boolean(ca-skeleton.tracing.enabled·ca-skeleton.grpc.enabled·ca-skeleton.fileserver.enabled·ca-skeleton.persistence-mongo.enabled·app.cache.redis.enabled), provider id(ca-skeleton.objectstorage.backend·app.messaging.broker), mode(app.cache.redis.client-mode) 메커니즘 official-vendor-doc / 정책 internal-policy 축 통일은 기존 5개 이상 config 클래스의 property 이름 변경 → breaking. 이행 창(legacy key 병행) 설계가 없으면 배포 중단 위험
D3 capability 활성화 property 는 단일 prefix 로 통일하고, 그 값은 env-keys.yaml 에 등록된 APP_* 환경변수에서만 주입한다 이미 APP_* 로 통일된 env registry 를 SSOT 로 유지 → property prefix 를 그에 맞춤. registry 를 포기하고 property 를 SSOT 로 삼는 대안은 sibling D2·D7 과 충돌하므로 선택 안 함 SPRING-EXTCONFIG-C7·C8·C9(raw/official-docs/config-spring-boot-externalized-configuration.md_ 치환 relaxed binding, canonical kebab-case 권장, @ConfigurationProperties prefix→bean 명명). sibling: raw/branch-notes/feature-env-driven-runtime-configuration D2(APP_ 전면 통일, registry=SSOT)·D7(verifyEnvKeys 3-way drift 검증) 메커니즘 official-vendor-doc / 정책 internal-policy (sibling 결정 상속) 현행 ca-skeleton.* 4종(grpc·fileserver·persistence-mongo·objectstorage)은 env-keys.yaml 에 row 가 0개 — 통일 전에 registry 등록이 선행돼야 하며 그 사이 verifyEnvKeys 가 실패할 수 있다
D4 production capability 는 명시적 선택으로만 활성화된다 — matchIfMissing = true·classpath 존재·local provider 기본값 셋 다 활성화 근거가 될 수 없다 (project note §36.3 규범 1 의 3 메커니즘 전부) 운영 표면·보안 표면·외부 연결을 만드는 capability 의 활성화 게이트 → 금지. 순수 관측(로깅 포맷 등) 또는 안전한 no-op 기본값 → 예외 허용하되 사유를 registry row 에 남김. mode 축의 기본값 선택은 활성화 조건이 아니므로 D4 적용 밖 — 예: app.cache.redis.client-mode=managed(matchIfMissing=true)는 이미 활성화된 capability 안에서 구현 방식을 고르는 것이고, 실제 활성화 게이트는 app.cache.redis.enabled(matchIfMissing=false)다 메커니즘: SB-COND-JD-C2(raw/official-docs/spring-boot-conditional-on-property-javadoc.md#SB-COND-JD-C2 — "If the property is not contained in the Environment at all, the matchIfMissing() attribute is consulted. By default missing attributes do not match."), SB-COND-JD-C1(#SB-COND-JD-C1matchIfMissing Defaults to false). 즉 프레임워크 기본값이 이미 fail-closed 이고, = true 는 그 기본값을 명시적으로 뒤집는 행위다. SB-COND-C2(reference 페이지 — attribute 존재만 확인, L0)는 보조. 금지 대상 근거는 internal-code-fact: adapter/inbound/grpc/.../GrpcServerConfig.java:18-22(gRPC 서버 기본 ON), adapter/outbound/objectstorage/.../ObjectStorageConfig.java:32-37(filesystem object storage 기본 ON), 그 외 TracingConfig.java:56-60·OutboxConfig.java:33-36·RedisCacheAdapterConfig.java:30-33·DistributedLockPersistenceConfig.java:25-29 메커니즘 official-vendor-doc(javadoc) / 금지 정책 internal-policy — Javadoc 이 기본값 false 와 "부재 시 미매칭"을 명시하므로 메커니즘은 확정. 다만 Javadoc 은 언제 이 attribute 를 쓰라/쓰지 말라 는 권고를 담지 않으므로, production capability 에 대한 금지 자체는 내부 정책이다 (2026-07-28 Javadoc 수집으로 통째 UNSUPPORTED_DECISION 에서 승급) 6곳 중 lock local provider(havingValue="false" + matchIfMissing=true)는 의도된 single-instance 기본값일 수 있다 — 일괄 금지 적용 전 개별 판정 필요(§Claims To Verify 1행). 또한 Javadoc 기본값이 false 라는 사실은 = true 를 쓴 6곳이 전부 의도적 override 였음을 뜻한다 — 각 override 의 원래 의도를 확인하지 않고 제거하면 기본 동작이 바뀐다
D5 활성화·안전 판정은 bean 이름 presence 가 아니라 typed descriptor 로 한다 판정 대상이 "그 계약을 실제로 구현했는가" → typed descriptor. 단순 "빈이 있는가" 로 충분한 내부 배선 → 기존 방식 유지 가능 SB-COND-C3(#SB-COND-C3 — 조건은 "지금까지 처리된 것" 기준 평가, bean definition 순서에 매우 주의), SB-COND-C4(#SB-COND-C4 — 그래서 bean 조건은 auto-configuration 클래스에서만 권고). internal-code-fact: app-bootstrap/.../runtime/StartupSafetyValidator.java:23-30 이 문자열 5개(distributedLockProvider·cacheStampedeProtection·outboxLeaderElection·distributedRateLimiter·migrationStartupRunner)의 containsBean 으로 판정. sibling: raw/branch-notes/feature-env-driven-runtime-configuration D8 (동일 메커니즘을 자기 노트에서 이미 UNSUPPORTED_IMPL_DECISION 으로 라벨) official-vendor-doc — 순서 의존 위험은 벤더 진술 이름이 같은 아무 bean 이나 있으면 안전으로 판정된다. descriptor 전환 전까지 multi-instance 안전 보고는 신뢰할 수 없다
D6 토폴로지 검증은 context refresh 완료 에 실행하고 실패 시 컨텍스트 기동을 거부한다 — ApplicationRunner/CommandLineRunner 로 옮기지 않는다 검증이 bean 실체를 봐야 함 + 실패 시 기동 거부가 필요 → refresh 전 singleton 초기화 시점. 기동 후 1회성 작업 → Runner SB-AVAIL-C4(raw/official-docs/spring-boot-application-availability-startup.md#SB-AVAIL-C4 — Runner 는 "once SpringApplication has started" 실행), SB-AVAIL-C3(#SB-AVAIL-C3 — Liveness 이벤트는 ApplicationStartedEvent(refresh 이후) 다음), SB-AVAIL-C5(#SB-AVAIL-C5 — startup 예외는 ApplicationFailedEvent 별도 경로). sibling: env-driven D8 의 trade-off(EnvironmentPostProcessor 는 bean 정의 이전이라 presence 검사 불가) official-vendor-doc (시점) + internal-policy (거부 정책) 검증 위치는 유지되고 판정 방식만 D5 로 바뀐다. 두 결정을 한 PR 에서 섞으면 회귀 원인 분리가 어렵다
D7 readiness 반영 여부는 descriptor 의 readinessImpact 로 결정하고, health group 구성은 그 값에서 파생한다 optional·fallback 가능 capability → readiness 제외. 실패 시 요청을 올바르게 처리할 수 없는 capability(session·strict idempotency·필수 lock·필수 producer) → readiness 포함 SB-AVAIL-C2(#SB-AVAIL-C2 — readiness = 트래픽 라우팅 게이트), SB-HEALTH-C9(raw/official-docs/runtime-health-spring-actuator-groups.md#SB-HEALTH-C9management.endpoint.health.group.<name> 으로 indicator include/exclude), SB-HEALTH-C11(#SB-HEALTH-C11 — group 은 StatusAggregator 상속, override 가능), SB-HEALTH-C12(#SB-HEALTH-C12 — 비필수 외부 시스템은 포함하지 말 것, essential 판단은 개발자 책임) 메커니즘 official-vendor-doc / 어떤 capability 가 essential 인지는 internal-policy SB-HEALTH-C8needs-confirmation 으로 남아 있다 — "default readiness group 이 외부 의존성을 포함하지 않는다" 의 단일 문장 verbatim 미확보. C12 가 부분 해소했을 뿐이다
D8 모든 capability 는 R0~R3 readiness 등급을 갖고, 등급 주장에는 등급별 증거가 필요하다 실제 provider 없이 타입·seam 만 → R0. 단일 로컬 프로세스에서만 검증 → R1. 실서비스 통합 + 동시성·실패 test + runbook 확보 → R2. cluster·failover·rolling upgrade 검증 → R3. 증거가 다음 등급에 못 미치면 낮은 등급 유지가 기본이고 승급 주장은 금지 조사 범위(Spring 공식 4건) 내 대응 근거 미확보 — production-readiness/maturity 계열은 이번 회차에서 조사하지 않았다(비존재 단정 아님). 근거는 internal-design-doc: ca-tmpl docs/superpowers/specs/2026-07-26-production-capability-platform-design.md §6 UNSUPPORTED_DECISION (조사 범위 내 미확보 + 내부 설계 결정) 등급 판정이 사람의 선언에 의존한다. 자동 검사가 없으면 R0 를 R2 로 적는 것을 막지 못한다 — §구현 가이드 5 의 gate 가 유일한 방어
D9 capability card 는 보증과 비보증을 둘 다 적고, 흔한 unsafe recipe 와 그 안전한 대체를 쌍으로 제시하며, 그 경합을 재현하는 test 이름을 명시한다 모든 R2 이상 capability → 필수. R0/R1 → card 없이 한계 문서화만 조사 범위 내 미확보 — 프로젝트 고유 요구이며 대응 외부 표준을 이번 회차에서 조사하지 않았다. 근거는 internal-design-doc(platform 설계 §6) + 사용자 요구("함수를 쓰다 부딪히는 문제를 개발자가 알 수 있게") UNSUPPORTED_DECISION (내부 결정) card 작성이 형식적으로 흐를 위험. "경합 test 이름 명시" 를 필수 칸으로 둔 것이 유일한 강제 장치다
D10 startup 실패는 기존 registry 어휘를 재사용하고 새 error code 를 만들지 않는다 descriptor 검증 실패 → STARTUP_VALIDATION_FAILED. 필수 provider 부재 → REQUIRED_ADAPTER_DISABLED. prod 금지 토글 → PROFILE_MISMATCH. 비활성 adapter 런타임 호출 → ADAPTER_DISABLED internal-code-fact: docs/registries/error-codes.yamlSTARTUP_VALIDATION_FAILED(L841)·REQUIRED_ADAPTER_DISABLED(L855)·PROFILE_MISMATCH(L889) owner_branch feature-migration-startup-contract, ADAPTER_DISABLED(L875) owner_branch feature-integration-adapter-templates. 코드: app-bootstrap/.../runtime/startup/StartupFailures.java internal-code-fact 본 branch 는 소비자다. 의미 확장이 필요하면 owner branch 의 registry 변경 절차를 거쳐야 하며 여기서 단독 결정할 수 없다
D15 capability card 의 필수 항목은 project note §36.2 원문을 그대로 상속하고 본 branch 는 그 목록을 narrow 하지 않는다. D9 는 그중 강제력이 약해 별도 규범이 필요한 3개만 추가로 못박는다 13개 항목 전체 → §36.2 상속. 보증·비보증 동시 기재 / unsafe recipe ↔ 안전한 대체 / 경합 test 이름 → D9 가 추가 규범. 기본 실패 정책·override → D12 가 값 집합 소유 project note §36.2 (13항목) internal-policy (project note 상속 — 재진술 아님) 상속 선언은 §36.2 가 바뀌면 자동 따라간다. 반대로 §36.2 개정 시 본 branch 가 알아채는 장치는 없다 — /sync 의 역참조 전파에 의존
D12 project note §11 의 실패 정책 매트릭스를 그대로 채택하고, descriptor 는 단일 failureMode 가 아니라 (실패 종류 → 정책 kind) 맵을 갖는다. provider 가 배정된 정책과 다른 kind 를 선언하면 startup 을 거부한다 매트릭스는 (capability × 실패 종류) 조합이다 — cache 는 backend 실패·codec 결함 2행, outbox 는 append·dispatcher 2행, rate limit 은 strict·availability 2행, notification 은 best-effort·durable 2행, lock 은 efficiency·fenced 2행. 따라서 capability 하나가 여러 행에 대응하며 단일 enum 파생은 성립하지 않는다. 어느 행에도 해당하지 않는 조합 → 매트릭스에 행을 추가하는 것이 선행(본 branch 가 §11 owner). 값이 아니라 선택지·파라미터인 2행(efficiency lock = "선언된 use case 정책에 따라", strict rate limit = "fail closed 또는 bounded local emergency limiter")은 정책 kind delegate-to-use-case 로 표기하고 실제 값은 use case 선언을 참조한다 project note §11 "Capability 별 실패 정책 매트릭스"(owner 선언 + 12행). internal-design-doc: ca-tmpl platform 설계 §7 "Failure policy is capability-specific, not globally 'fail open' or 'fail closed'." internal-policy (project note §11 상속 — 재진술 아님, 값 집합 파생과 검증만 본 branch 소유) 12행이 앞으로 등장할 capability 를 전부 덮는지 미검증. 매트릭스에 없는 조합이 나오면 provider 는 startup 을 통과하지 못하고 §11 개정이 선행돼야 한다 — 이 경직성은 의도된 것이나 이행 초기에 마찰을 만든다
D13 비활성 capability 는 연결·백그라운드 워커·스키마·health contributor 를 만들지 않으며, 이를 negative fixture 로 검증한다 모든 선택 가능 capability → 기본 적용. classpath 존재만으로 auto-configuration 이 도는 third-party starter → 별도 배제 설정이 필요하며 그 사실을 registry row 에 남김 project note §36.3 규범 2 ("미사용 capability 는 연결·백그라운드 워커·스키마·암묵적 런타임 동작을 만들지 않는다"). 메커니즘: SB-COND-C1(조건 미충족 시 configuration 미포함) 정책 internal-policy (project note 상속) / 메커니즘 official-vendor-doc @ConditionalOnProperty본 프로젝트가 정의한 bean 만 막는다. third-party starter 의 auto-configuration 이나 모듈 classpath 존재로 생기는 부수 효과는 이 조건으로 막히지 않는다 — negative fixture 가 그 차이를 드러내야 한다
D14 이행 창 동안 legacy 활성화 키를 병행할 수 있으나, legacy 값과 SSOT 값이 불일치하면 startup 오류(STARTUP_VALIDATION_FAILED)로 처리한다 키 rename·축 통일(D2·D3) 이행 중 → 병행 허용 + 불일치 시 거부. 이행 완료 후 → legacy 키 제거, 잔존 시 startup 오류 project note §36.3 규범 3 ("legacy flag 를 한시적으로 유지하는 경우, SSOT 와의 불일치는 startup 오류다"). 실패 코드는 D10 재사용 internal-policy (project note 상속) 병행 메커니즘은 미정 — §구현 가이드 2 의 UNSUPPORTED_IMPL_DECISION 참조. 불일치 판정 자체는 이 결정으로 확정되나, 두 키를 어떻게 동시에 읽을지는 별개
D11 capability descriptor registry 는 registry-governance 의 공통 스키마를 따르고, 기존 capabilities.yaml다른 파일로 둔다 신규 registry family → universal 3 column(owner_branch·compatibility_impact·required_test) + family identity column 필수. 기존 파일 확장은 D1 의 어휘 충돌을 registry 층까지 끌고 오므로 선택 안 함 sibling: raw/branch-notes/feature-contract-registry-governance D1(registry 없이 추가 금지)·D4(universal 3 column)·D6(구현 artifact 는 ca-tmpl/docs/registries/ yaml) internal-policy (sibling 결정 상속) 신규 registry 파일명은 D1 의 어휘 확정에 종속 → §구현 가이드 1 의 UNSUPPORTED_IMPL_DECISION 과 같은 미결 항목

구현 가이드

3-rule meta principle 적용 — R1 각 sub-section 은 Decision ID + Claim ID reference, R2 근거 없는 detail 은 UNSUPPORTED_IMPL_DECISION + trade-off 한 줄, R3 본 branch 결정 범위 밖은 이관.

코드 위치 표기: ca-tmpl repo 기준 상대 경로. 본 branch 는 계약 SSOT 이고 실제 코드 작성은 ca-tmpl plan 이 소유한다 — 아래 클래스명·경로는 planned 이며 코드로 확인된 것만 actually-implemented 로 표기했다.

1. Descriptor 형태와 registry row

Trace: D1(어휘 분리) · D11(registry 스키마) · sibling raw/branch-notes/feature-contract-registry-governance D4

  • descriptor 기여·수집 메커니즘 (근거 있음 — 라벨 불필요): provider 가 자기 descriptor 를 기여하고 app-bootstrap수집·검증한다. 근거 internal-design-doc: ca-tmpl platform 설계 §7 Capability descriptors — "Each active provider contributes a typed descriptor with at least:" (L318) 및 "app-bootstrap validates descriptors and selected settings. It does not accept arbitrary beans with a magic name." (L330). 즉 판정 입력은 런타임 객체이지 registry yaml 이 아니다.
  • registry row 와 런타임 descriptor 의 역할 분리: registry row(D11)는 governance 기록 — 어떤 capability 가 존재하고 누가 owner 이며 어떤 test 가 강제하는지. 런타임 descriptor 는 판정 입력 — 이번 기동에서 어떤 provider 가 실제로 활성이고 무엇을 보증하는지.
  • 둘의 대조는 startup 이 아니라 build/test 단계다 (2026-07-28 정정). 근거 internal-code-fact: registry yaml 은 런타임 resource 로 packaging 되지 않는다(src/build.gradle 에 registries → resources 복사 0건). 기존 drift 검사도 전부 빌드/테스트 시점이다 — verifyEnvKeys 는 Gradle task 로 ${rootProject.projectDir}/../docs/registries/env-keys.yaml 을 읽고(src/build.gradle:808-816), RepositoryAccessCapabilityRegistryTest/docs 가 gitignore 라 파일 부재 시 Assumptions.assumeTrueSKIP(:26-27, :53-55). 따라서 registry 불일치는 test 실패이지 STARTUP_VALIDATION_FAILED 가 아니다.
  • startup 이 대조하는 것은 descriptor ↔ 선택된 설정이다 — 활성 provider 의 descriptor 가 선언한 보증·multi-instance 지원·필요 backing role 이 현재 설정과 맞는지. 여기서의 불일치만 STARTUP_VALIDATION_FAILED(D10)다.
  • UNSUPPORTED_IMPL_DECISION: 런타임 축의 어휘와 registry 파일명. 후보는 runtime-capability / provider-capability / runtime-feature 이며 어느 것도 외부 근거가 없다. trade-off — runtime-capability 는 기존 capabilities.yaml(use-case capability)과 낱말이 겹쳐 grep 혼선을 남기고, runtime-feature 는 sibling raw/branch-notes/feature-env-driven-runtime-configuration D9 의 "feature flag" 와 겹친다. 사용자 확정 전까지 어느 쪽도 코드에 박지 않는다.

descriptor 필수 필드 (planned):

필드 의미 값 출처
capabilityId 안정 식별자. rename 은 breaking 신규 registry row
providerId 선택된 구현체 식별자 활성화 property 값 (D2)
readiness R0|R1|R2|R3 (D8) provider 가 선언, gate 가 검증
guarantee 이 provider 가 보장하는 것 (문자열 enum) capability 별 branch 가 정의
failurePolicy (실패 종류 → 정책 kind) 맵. 단일 값이 아니다 — §11 매트릭스는 capability 당 1행이 아니라 (capability × 실패 종류) 조합이기 때문 (D12) project note §11 매트릭스에서 파생
multiInstanceSupported multi-instance 에서 유효한가 D5 판정 입력
requiredBackingRole 필요한 백엔드 역할 (예: Redis coordination role) 기술 런타임 branch 가 정의
readinessImpact none|degraded|unready (D7) health group 파생 입력
implementationVersion 구현 버전 — rolling upgrade 판정용 빌드 정보

registry row 는 universal 3 column(owner_branch·compatibility_impact·required_test)을 추가로 갖는다 (sibling raw/branch-notes/feature-contract-registry-governance D4).

2. 활성화 property 규약

Trace: D2(축 단일화) · D3(prefix·env 정합) · D4(matchIfMissing 금지) · SB-COND-C1 · SPRING-EXTCONFIG-C7~C9

  • UNSUPPORTED_IMPL_DECISION: 통일할 prefix 문자열(ca-skeleton.* 로 모을지 app.* 로 모을지). trade-off — app.*APP_ 환경변수와 relaxed binding 이 1:1(APP_CACHE_REDIS_ENABLEDapp.cache.redis.enabled)이라 registry 정합 비용이 낮고, ca-skeleton.* 은 프로젝트 식별성이 높지만 CA_SKELETON_* 환경변수가 APP_* 통일(sibling D2)과 충돌한다. 정합 비용만 보면 app.* 이 우세하나 결정 근거는 없다.
형태 적용 조건
provider <prefix>.<capability>.provider = disabled | <providerId> 구현체 대안이 2개 이상
mode <prefix>.<capability>.<mode-name> = disabled | <mode> provider 는 하나이고 동작 방식만 갈림
boolean <prefix>.<capability>.enabled = true | false 켜고 끄는 것 외 선택지 없음

규범:

  • 한 capability 에 두 축을 동시에 두지 않는다 (D2).
  • 모든 활성화 키는 env-keys.yaml row 를 가지며 APP_* 환경변수에서 주입된다 (D3). registry row 없는 키는 verifyEnvKeys 가 실패시킨다 (sibling env-driven D7).
  • production capability 에는 matchIfMissing = true 를 쓰지 않는다 (D4). 프레임워크 기본값이 이미 false 이므로(SB-COND-JD-C1) 속성 부재 = 비활성은 별도 조치 없이 성립한다.
  • 나머지 두 활성화 경로도 막는다 (D4, §36.3 규범 1): classpath 존재는 D2 의 축 선언이 없으면 provider 로 인정하지 않으며, local provider 기본값(예: filesystem object storage)은 다른 provider 와 동일하게 명시 선택을 요구한다 — 기본 provider 라는 이유로 matchIfMissing 을 붙이지 않는다.
  • 표기 계약: matchIfMissing = false명시한다 — 생략해도 의미는 같다(기본값 false).

UNSUPPORTED_IMPL_DECISION: 위 "명시 vs 생략" 표기 규약. as-built 는 혼재다 — main 소스의 @ConditionalOnProperty 22곳 중 matchIfMissing 을 지정한 것은 11곳(= true 6 / = false 5)이고 나머지 11곳은 생략이다. 즉 생략이 명시-false 보다 많다. trade-off — 명시는 장황하지만 self-documenting 이고 = true 를 실수로 넣었을 때 diff 에서 눈에 띈다. 생략은 간결하고 기본값을 신뢰하지만, 검토자가 Javadoc 기본값을 알아야 의도를 판정할 수 있다. as-built 다수는 생략이므로 정합만으로는 tie-break 가 안 되고, 어느 쪽도 외부 근거는 없다.

  • placeholder 참조는 canonical kebab-case 로 쓴다 (SPRING-EXTCONFIG-C8).
  • 이행 창 동안 legacy 키를 병행하되 SSOT 값과 불일치하면 startup 을 거부한다 (D14).

UNSUPPORTED_IMPL_DECISION: legacy 키 병행 메커니즘. 후보 — (a) application.yml 에 두 키를 모두 두고 새 키를 SSOT 로 읽기, (b) EnvironmentPostProcessor 로 legacy → 신규 alias 주입, (c) legacy 키 사용 시 deprecated 경고만 남기고 값은 신규 키에서만 읽기. trade-off — (a)는 가장 단순하나 verifyEnvKeys 의 3-way drift 검사(sibling env-driven D7)를 이행 기간 동안 우회해야 하고, (b)는 registry 를 건드리지 않지만 relaxed binding 과 alias 가 겹칠 때 우선순위가 불투명하며, (c)는 깔끔하나 legacy 키만 설정한 배포를 조용히 비활성으로 만들어 D14 의 "불일치 = 오류" 와 충돌한다. (a)가 D14 와 가장 정합하나 결정 근거는 없다.

3. Startup 검증 배치와 실패 매핑

Trace: D5(typed descriptor) · D6(검증 시점) · D10(실패 어휘) · SB-COND-C3/C4 · SB-AVAIL-C3/C4/C5

  • 검증 위치는 현행을 유지한다 — SmartInitializingSingleton (refresh 완료 전, 모든 singleton 초기화 후). ApplicationRunner 는 "이미 시작된 후" 실행되므로 부적합하다 (SB-AVAIL-C4).
  • 판정 입력만 교체한다: beanFactory.containsBean("<name>") → 등록된 descriptor 집합. actually-implemented 현행은 app-bootstrap/.../runtime/StartupSafetyValidator.java:23-30(이름 목록) + :78-79(containsBean 호출 루프) — 교체 대상은 둘 다.
  • 실패 매핑 (D10 — 전부 기존 registry code):
조건 code 기존 owner
descriptor 스키마·정합성 위반 STARTUP_VALIDATION_FAILED feature-migration-startup-contract
필수 capability 의 provider 부재 또는 보증 미달 REQUIRED_ADAPTER_DISABLED feature-migration-startup-contract
prod 프로파일에서 금지 토글 활성 PROFILE_MISMATCH feature-migration-startup-contract
비활성 capability 를 런타임에 호출 ADAPTER_DISABLED feature-integration-adapter-templates

3-1. 실패 정책 kind 와 매핑 규칙

Trace: D12 · project note §11 (재진술 아님 — 값 집합 파생과 검증만 소유)

  • UNSUPPORTED_IMPL_DECISION: 정책 kind 의 명칭. 아래 7개는 §11 12행의 동작을 축약한 것이며 외부 근거가 없다. trade-off — 축약하면 descriptor 가 짧아지지만 §11 문장과 1:1 대조가 어려워지고, 문장을 그대로 값으로 쓰면 대조는 쉬우나 오타·표기 흔들림이 생긴다.
정책 kind §11 대응 행 의미
fail-open optional cache(backend 실패), best-effort notification·message source 로 fallback 또는 무시하고 진행, degraded 신호
fail-closed cache codec 결함, session store, keyed mutation idempotency, fenced correctness lock, strict rate limit(기본) 해당 연산을 실패시킨다
degrade-with-fallback availability 지향 rate limit 제한된 local fallback 으로 계속하되 degraded 신호 필수, 조용한 무제한 통과 금지
rollback-transaction outbox append 업무 트랜잭션을 롤백
accumulate-and-alert outbox dispatcher 장애 write 는 계속 축적, lag·backlog 알림
durable-retry durable notification·message outbox/inbox + 재시도 + 종단 실패 경로
delegate-to-use-case efficiency lock, strict rate limit(변형 선택 시) 값이 아니라 선택지 — 실제 동작은 use case 선언이 정한다

규범: descriptor 의 failurePolicy 맵 키는 실패 종류(예: backend-unavailable, codec-error, append, dispatch)이고 값은 위 kind 다.

대조축의 시점 배정 (§구현 가이드 1 이 세운 "governance 문서는 런타임 입력이 아니다" 원칙을 그대로 적용):

대조 시점 실패 표현
§11 매트릭스 ↔ descriptor 선언 — 그 (capability × 실패 종류) 조합이 매트릭스에 존재하는가, 배정된 kind 와 같은가 build/test (§11 은 런타임 로딩되지 않는다) contract test 실패
descriptor 선언 ↔ 선택된 설정 — 예: degrade-with-fallback 인데 fallback 설정이 없음 startup STARTUP_VALIDATION_FAILED (D10)

이 분리로 app-bootstrap 에 §11 12행을 상수 테이블로 재진술할 필요가 없다 — D12 의 Reference-Only 를 코드 층까지 유지한다.

UNSUPPORTED_IMPL_DECISION: build/test 대조의 구현 위치. 후보 — (a) 기존 registry drift test 패턴을 따라 app-bootstrap test 에 두기(단, /docs 가 gitignore 라 파일 부재 시 SKIP 되는 한계를 그대로 물려받는다), (b) verifyEnvKeys 처럼 Gradle task 로 두기(CI 에서 확실히 돌지만 IDE 피드백이 늦다). trade-off 는 "기존 패턴 일관성" vs "SKIP 되지 않는 강제력" 이며 어느 쪽도 외부 근거가 없다.

3-2. 비활성 capability 의 리소스 미생성 검증

Trace: D13 · SB-COND-C1

  • UNSUPPORTED_IMPL_DECISION: third-party starter 배제 수단. 후보 — (a) spring.autoconfigure.exclude 목록, (b) 해당 starter 를 optional 모듈에만 두고 core classpath 에서 제외, (c) 배제 없이 negative fixture 로 검출만. trade-off — (a)는 즉시 적용되나 클래스명 하드코딩이 버전 업그레이드에 취약하고, (b)는 근본적이나 모듈 registry 변경(19-leaf 고정)을 건드리며, (c)는 비용이 없지만 문제를 막지 못하고 알려주기만 한다.

negative fixture 가 assert 할 4종 (D13 의 "연결·워커·스키마·health contributor"):

대상 관측 방법
연결 해당 provider 의 client/connection factory bean 부재
백그라운드 워커 scheduler·listener·relay bean 부재 및 스레드 미생성
스키마 해당 capability 소유 테이블·인덱스 DDL 미실행
health contributor /actuator/health component 목록에 해당 이름 부재

registry row 의 required_test 에는 위 fixture 이름을 적는다 (sibling raw/branch-notes/feature-contract-registry-governance D2 — "registry 항목은 최소 1개 이상의 contract test 와 연결").

4. Readiness 반영 규칙

Trace: D7 · SB-AVAIL-C2 · SB-HEALTH-C9/C11/C12 · as-built application.yml:250-251

  • 현행 (as-built): readiness group 은 정적 목록이다 — src/app-bootstrap/src/main/resources/application.yml:250-251readiness: include: readinessState,db. 소유 주석은 같은 파일 L236-237 로 raw/branch-notes/feature-runtime-health-lifecycle-contract 를 가리킨다.
  • 시점 문제 (미확인 가정): descriptor 는 SmartInitializingSingleton(refresh 완료 전 후반, D6)에 수집되는 반면 health group 은 property 바인딩으로 구성된다 — 두 시점의 선후는 수집한 자료로 확인되지 않았다. 수집한 actuator 문서는 group 의 property 구성(SB-HEALTH-C9)·aggregation(C10/C11)·essential 판단(C12)만 담고 group 구성 시점이나 프로그래매틱 확장점을 다루지 않는다. 이 가정이 틀리면 아래 후보 (a)·(c)의 기각 근거가 무너진다 → §Claims To Verify.
  • UNSUPPORTED_IMPL_DECISION: 파생 메커니즘. 후보 — (a) EnvironmentPostProcessor 가 descriptor 선언을 읽어 include 목록을 주입, (b) 정적 yaml 을 유지하고 descriptor 와의 일치를 test 로 강제, (c) 커스텀 group registry 를 직접 구현. trade-off — (a)는 시점을 맞추지만 descriptor 를 bean 이전에 알아야 해서 D5 의 typed 판정과 다른 소스(설정)를 봐야 하고, (b)는 구현이 가장 작고 D6 시점과 충돌하지 않으나 설정 누락을 배포 후에야 test 로 잡는다, (c)는 자유도가 크지만 Spring 의 group 계약을 재구현하는 비용이 든다(미확인 — 프로그래매틱 확장점 존재 여부를 확인하지 않았다). 비용만 보면 (b)가 우세하나 결정 근거는 없다.
  • db 처럼 capability 모델 밖 indicator: 현행 include 의 db 는 Spring Boot DataSource 가 자동 기여하는 indicator 로 본 branch 의 descriptor 모델에 대응 행이 없다. capability 모델 밖 indicator 의 group 편입은 raw/branch-notes/feature-runtime-health-lifecycle-contract D10(Required vs Optional Dependency Matrix) 소유이며 본 branch 는 건드리지 않는다.
  • readinessImpact: unready 인 활성 capability 만 readiness group 에 포함한다. group 구성은 management.endpoint.health.group.readiness 의 include 목록으로 표현한다 (SB-HEALTH-C9).
  • readinessImpact: degraded 는 component health 에는 나타나되 readiness group 에는 넣지 않는다 — 비필수 외부 시스템을 readiness 에 넣지 말라는 기준 (SB-HEALTH-C12).
  • 비활성 capability 는 health contributor 를 등록하지 않는다. 등록되지 않은 것은 aggregation 에도 참여하지 않는다 (SB-HEALTH-C10).
  • liveness group 에는 어떤 capability 도 넣지 않는다 — liveness 는 프로세스 자체 신호다 (SB-AVAIL-C1).

OUT_OF_BRANCH_SCOPE: health endpoint 의 shape·노출·인증raw/branch-notes/feature-runtime-health-lifecycle-contract D2 와 raw/branch-notes/feature-management-actuator-security-contract 소유다. 본 branch 는 어떤 capability 가 어느 group 에 들어가는지 의 판정 입력만 정한다.

5. Readiness 등급과 card 게이트

Trace: D8 · D9

  • UNSUPPORTED_IMPL_DECISION: 등급 승급의 자동 검사 방법. 후보는 (a) descriptor 의 readiness 값과 test 태그를 대조하는 Gradle 검사, (b) card 문서의 필수 칸 존재를 검사하는 린터, (c) 리뷰 체크리스트(수동). trade-off — (a)만이 위조를 막지만 test 태그 규약이 선행돼야 하고, (c)는 즉시 가능하나 D8 의 Open Risk 를 해소하지 못한다.
  • R2 이상을 주장하려면 card 에 보증과 비보증이 둘 다 있어야 한다 (D9).
  • card 의 "unsafe recipe ↔ 안전한 대체" 칸에는 그 경합을 재현하는 test 이름을 적는다. test 이름이 없으면 그 보증은 card 에 쓸 수 없다.
  • 등급별 요구 증거는 project note §36.1 을 상속한다 — 본 branch 는 재진술하지 않는다.
  • card 의 나머지 항목(owner 모듈·semantic port / provider ID·readiness 등급 / 필요한 토폴로지·영속·축출 정책 / 설정 키·secret / liveness·readiness 영향 / bounded cardinality metric·trace span / 적합·부적합 지침·비용 모델·자원 한계 / 집중·통합·실패 test 명령 / rolling upgrade·복구 노트 / runbook 링크)도 project note §36.2 원문을 그대로 상속한다. 본 branch 는 그 목록을 narrow 하지 않으며, D9 는 그중 강제력이 약해 별도 규범이 필요한 3개(보증·비보증 동시 기재 / unsafe recipe ↔ 안전한 대체 / 경합 test 이름)만 추가로 못박는다.
  • card 의 "기본 실패 정책과 허용되는 override" 항목은 D12 가 정한 값 집합을 쓴다 — §11 매트릭스 12행에서 파생된 enum 외의 값은 card 에 쓸 수 없다.

6. 현행 → 목표 이행

Trace: D2·D3·D4·D5 의 적용 대상. 각 항목의 현행 상태는 §Audit & Findings 의 코드 근거와 1:1.

단계 내용 선행 조건
1 미등록 활성화 키 4종(grpc·fileserver·persistence-mongo·objectstorage)을 env-keys.yaml 에 등록 sibling env-driven D7 절차
2 notification dead config 해소 — 조건 키와 yml 키 일치 (A3) owner 판정 필요 (§Audit A3)
3 matchIfMissing=true 6곳 개별 판정 → production capability 는 제거 D4
4 활성화 축 통일 + prefix 통일 (legacy key 병행 창 포함) D2·D3, 어휘·prefix 확정
5 descriptor 도입 후 StartupSafetyValidator 판정 입력 교체 D5, 1~4 완료

OUT_OF_BRANCH_SCOPE: 각 capability 의 provider 구현(Redis role, Kafka producer, session store 등)은 Tier T/C branch 소유다. 본 §은 활성화·판정·readiness 반영의 공통 계약만 다룬다.

엣지·실패·의존

  • 실패·엣지 경로

    • descriptor 를 선언하지 않은 provider 가 활성화됨 → STARTUP_VALIDATION_FAILED 로 기동 거부 (D10). 조용히 통과시키면 D5 가 해결하려는 문제가 그대로 남는다.
    • 같은 capability 에 두 provider 가 동시에 활성 → 기동 거부. @ConditionalOnProperty 는 상호 배타를 보장하지 않으므로 descriptor 수집 시점에 중복을 검사해야 한다 (D2).
    • 활성화 property 는 켜져 있으나 필요한 backing role 설정이 없음 → REQUIRED_ADAPTER_DISABLED.
    • descriptor 는 multiInstanceSupported: false 인데 APP_MULTI_INSTANCE_ENABLED=true → 기동 거부 (H8 이전 대상 계약).
    • 비활성 capability 의 port 를 use case 가 호출 → ADAPTER_DISABLED (런타임 실패, 기동 실패 아님).
    • readiness group 에 넣을 capability 가 하나도 없음 → capability 유래 항목만 비우고 기동은 계속. readinessState 는 capability 가 아니므로 항상 유지한다(제거하면 k8s readiness probe 게이트 자체를 잃는다). readiness 가 항상 UP 이 되는 것을 정상으로 문서화한다 (SB-HEALTH-C12 의 판단 기준).
    • 검증 자체가 실패로 기동을 막을 때 ApplicationFailedEvent 경로로 빠지며 availability state 는 설정되지 않는다 (SB-AVAIL-C5) — 로그만이 유일한 진단 수단이다.
  • 다른 계약 의존

Audit & Findings

2026-07-28 /branch-spec 의 §2 ground truth 확인에서 ca-tmpl 코드·registry·yml 을 직접 읽어 발견한 정합 문제. 전부 actually-implemented 사실(경로·행 명시)이며 추측이 아니다. 각 항목은 §구현 가이드 6 의 이행 단계와 1:1 대응한다.

본 branch 가 단독으로 고칠 수 없는 항목은 owner 를 명시했다 — 자동 수정하지 않고 정합 권고만 남긴다.

ID 발견 근거 (ca-tmpl 상대 경로) 판정 owner
A1 활성화 규약 4종 혼용 — boolean enabled / provider id backend·broker / mode client-mode 가 동시에 쓰인다 GrpcServerConfig.java:18-22, FileExportConfig.java:26-29, MongoPersistenceConfig.java:21-24, ObjectStorageConfig.java:32-37, KafkaAdapterConfig.java:23, RedisCacheAdapterConfig.java:30-38 ACTIVATION_AXIS_DRIFT — D2 로 통일 대상 본 branch
A2 property prefix 이중화ca-skeleton.*app.* 이 공존. 반면 env registry 는 APP_* 로 통일됨 위 파일들 + docs/registries/env-keys.yaml(108행 중 106행이 APP_ 105 + SPRING_PROFILES_ACTIVE 1. 예외 2건은 third-party 소유 키 — MANAGEMENT_SERVER_PORT(L72)·OTEL_EXPORTER_OTLP_ENDPOINT(L711)) PREFIX_DRIFT — D3 대상. prefix 문자열은 미확정 본 branch (+ env-driven 협의)
A3 notification dead config — 어댑터는 app.notification.google-email.enabled / app.notification.slack-webhook.enabled 로 조건 평가하나, application.yml 과 registry 에는 app.notification.{slack,email}.provider 만 존재. matchIfMissing=false 이므로 문서화된 설정으로는 두 어댑터가 활성화되지 않는다 GoogleEmailNotificationAdapterConfig.java:19, SlackNotificationAdapterConfig.java:19 vs app-bootstrap/src/main/resources/application.yml:500-506, env-keys.yaml(APP_NOTIFICATION_SLACK_PROVIDER·APP_NOTIFICATION_EMAIL_PROVIDER 2행) DEAD_CONFIG — 활성화 불가 raw/branch-notes/feature-notification-provider-spi 와 협의 필요
A4 registry 미등록 활성화 키 4종 — grpc·fileserver·persistence-mongo·objectstorage 관련 키가 env-keys.yaml0개 grep -c "name: APP_{GRPC,FILESERVER,MONGO,OBJECTSTORAGE}" docs/registries/env-keys.yaml = 0 REGISTRY_GAP — sibling raw/branch-notes/feature-contract-registry-governance D1("registry 없이 추가하지 않음") 위반 상태 본 branch (등록) + registry-governance (절차)
A5 속성 부재로 활성화되는 운영 표면 3건 — gRPC 서버, filesystem object storage, outbox relay scheduler 가 아무 설정 없이 켜진다. 나머지 3곳(TracingConfig·RedisCacheAdapterConfig mode 축·DistributedLockPersistenceConfig local provider)은 관측 또는 mode 축·의도된 single-instance 기본값으로 D4 적용 밖(판정 근거는 §Claims To Verify 1행) GrpcServerConfig.java:18-22, ObjectStorageConfig.java:32-37, OutboxConfig.java:33-36(ca-skeleton.outbox.relay-enabled — DB 폴링 + broker 발행이라는 외부 연결을 만든다) IMPLICIT_ACTIVATION — D4 대상 본 branch (계약) + 각 adapter branch (적용)
A6 bean 이름 문자열 판정 — multi-instance 안전을 bean 이름 5개의 presence 로 판정. 같은 이름의 아무 bean 이나 있으면 안전으로 보고된다 StartupSafetyValidator.java:23-30 (REQUIRED_MULTI_INSTANCE_BEANS + containsBean) UNTYPED_SAFETY_CHECK — D5 대상. 공식 문서의 순서 의존 경고(SB-COND-C3)와도 어긋남 본 branch (H8 이전 수신) — 원 owner raw/branch-notes/feature-env-driven-runtime-configuration D8
A8 readiness group 이 정적 목록readiness: include: readinessState,db 가 yml 에 손으로 적혀 있고, db 는 Spring Boot DataSource 자동 기여 indicator 라 본 branch 의 descriptor 모델에 대응 행이 없다 src/app-bootstrap/src/main/resources/application.yml:250-251(소유 주석 L236-237 → raw/branch-notes/feature-runtime-health-lifecycle-contract) STATIC_READINESS_GROUP — D7 의 "descriptor 에서 파생" 과 시점·형태가 어긋난다. 파생 메커니즘은 §구현 가이드 4 의 UNSUPPORTED_IMPL_DECISION 본 branch (판정 입력) + raw/branch-notes/feature-runtime-health-lifecycle-contract (group shape·비-capability indicator)
A7 capability 어휘 충돌capabilities.yaml(7 row)과 @UseCaseCapability 가 use-case repository access 의미로 이미 점유 docs/registries/capabilities.yaml, application-core/.../application/capability/UseCaseCapability.java TERM_COLLISION — D1 로 분리. 명칭 미확정 본 branch

이관 기록 (H8, 2026-07-28) — multi-instance 활성화 판정 메커니즘의 owner 가 raw/branch-notes/feature-env-driven-runtime-configuration D8 에서 본 branch 로 이동했다. APP_MULTI_INSTANCE_ENABLED flag 자체·env-keys.yaml row·APP_ prefix 통일(D2)·drift 검증(D7)은 env-driven 이 계속 소유한다. 설계 근거: docs/superpowers/specs/2026-07-28-ca-skeleton-production-capability-feature-decomposition-design.md §5.1. 최초 설계는 이전 원천을 feature-background-job-async-contract 로 잘못 적었고 본 /branch-spec 에서 정정했다.

검증해야 할 주장

Claim Why uncertain How to verify Status
matchIfMissing=true 6곳의 D4 적용/비적용 분류가 옳다 — 특히 lock local provider 는 의도된 single-instance 기본값이고, client-mode 는 활성화 게이트가 아닌 mode 축이다 D4 를 일괄 적용하면 single-instance 기본 동작이 깨질 수 있는데, 그 의도가 코드 주석에만 있고 결정으로 박혀 있지 않다 DistributedLockPersistenceConfig.java:25-29havingValue="false" 조합을 raw/branch-notes/feature-distributed-lock-contract D3 와 대조 + RedisCacheAdapterConfig.java:30-40 의 두 조건(client-mode mode 축 / enabled 활성화 게이트) 분리 확인 needs-confirmation
notification 어댑터가 실제로 활성화된 적이 없다 (A3) 조건 키가 yml·registry 어디에도 없으나, 테스트나 별도 profile 에서 주입될 가능성을 배제하지 못했다 ca-tmpl 에서 grep -rn "google-email.enabled|slack-webhook.enabled" src/ 로 test·resources 포함 전수 확인 needs-confirmation
descriptor 수집이 SmartInitializingSingleton 시점에 완전하다 모든 singleton 초기화 후이지만, lazy bean 이나 조건부로 늦게 등록되는 provider 가 있으면 누락될 수 있다 descriptor 미등록 provider 를 심은 negative fixture 로 STARTUP_VALIDATION_FAILED 발생 확인 planned
readiness group 에서 제외된 degraded capability 가 실제로 트래픽 차단을 유발하지 않는다 SB-HEALTH-C11 상 group 이 aggregator 를 상속하므로, component health 의 DOWN 이 다른 경로로 readiness 에 전파될 여지가 남는다 Redis 를 내린 상태에서 /actuator/health/readiness 가 UP 을 유지하는지 통합 test planned
"default readiness group 은 외부 의존성을 포함하지 않는다" 공식 문서에서 단일 문장 verbatim 을 확보하지 못했다 (SB-HEALTH-C8). SB-HEALTH-C12 가 판단 기준만 부분 제공 Spring Boot Kubernetes probes 섹션 재수집 또는 실제 기본 group 멤버십을 런타임에서 확인 needs-confirmation
health group 구성 시점이 descriptor 수집(SmartInitializingSingleton)보다 이르다 §구현 가이드 4 의 후보 (a)·(c) 기각이 이 선후에 달려 있는데, 수집한 actuator 문서에 group 구성 시점·프로그래매틱 확장점 진술이 없다 Spring Boot actuator health-groups 문서 재수집(프로그래매틱 확장점 포함) 또는 실제 기동 순서를 로그로 확인 needs-confirmation
registry yaml 은 런타임에 읽히지 않는다 src/build.gradle 에 registries → resources packaging 이 0건임을 grep 으로 확인했으나, 다른 경로(예: jar 외부 마운트)로 읽힐 가능성을 배제하지 못했다 ca-tmpl 에서 grep -rn "docs/registries" src/ --include=*.java --include=*.gradle 전수 확인 needs-confirmation
본 branch 가 소유할 관심사가 sibling branch 결정과 겹치지 않는다 D-row 11개 작성 후에도 owner 경계가 문장으로만 존재 /sync 실행 — owner 중복·재진술 검출 needs-confirmation

Coverage / 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)

2026-07-28 1회차 coverage-auditor 판정 = Not-covered (Blocking 1 / Should-fix 3). 아래 표는 판정 시점 상태이며, 각 finding 의 보강을 같은 날 적용했다(우측 "1회차 보강" 열). 재판정 전까지 Covered 로 간주하지 않는다. governing: project note §11 · §25 · §36 + 분해 설계 §6 · §9

관심사 판정 시 상태 owner 심각도 1회차 보강
capability descriptor 최소 필드 9개 (§36.3) covered-here — (§36.3 과 1:1 일치 확인됨)
활성화 SSOT 축 선택 (provider/dispatch-mode/enabled) covered-here
활성화 규범① 암묵 활성화 금지 covered-here D4 선택 조건에 mode 축 경계 추가
활성화 규범④ typed descriptor 판정 covered-here 기여·수집 메커니즘을 §구현 가이드 1 에 명시
활성화 규범② 미사용 capability 리소스 미생성 missing (없음) 🟡 Should-fix D13 신설
활성화 규범③ legacy flag 불일치 = startup 오류 missing (없음) 🟡 Should-fix D14 신설 + §구현 가이드 2 에 병행 메커니즘 라벨
readiness 등급 R0~R3 + 승급 증거 covered-here D8 선택 조건을 등급 분기로 재작성
fail-fast 토폴로지 검증 시점/거부 covered-here
capability card 필수 항목 13개 (§36.2) covered-here (3/13) 🟡 Should-fix §구현 가이드 5 에 나머지 10개 상속 선언 + D12 연결
§11 실패 정책 매트릭스 12행의 owner missing (없음) 🔴 Blocking D12 신설 — 매트릭스 채택 + failurePolicy 맵 파생 (2회차 지적으로 단일 enum → 맵 정정) + 불일치는 build/test 대조
startup 실패 코드 매핑 delegated raw/branch-notes/feature-migration-startup-contract, raw/branch-notes/feature-integration-adapter-templates OK
descriptor registry 스키마·저장 위치 delegated raw/branch-notes/feature-contract-registry-governance OK
readiness endpoint shape/노출/인증 delegated raw/branch-notes/feature-runtime-health-lifecycle-contract, raw/branch-notes/feature-management-actuator-security-contract OK A8 로 정적 include 현행 기록 + 비-capability indicator 취급을 명시 위임
개별 capability 의 provider 구현 delegated 각 Tier T/C branch OK

위임 판정 주의: §11 매트릭스가 missing 인 이유는 위임 후보 6개 sibling(cache-strategy-registry / idempotency-ownership-protocol / fenced-lock-coordination / outbox-dispatch-mode / notification-durable-delivery / redis-runtime-role-isolation)이 전부 scaffolding 상태(D-row 0개)라 실제로 아무도 맡고 있지 않았기 때문이다. 그 branch 들이 /branch-spec 을 마치면 일부 행은 delegated 로 재분류될 수 있다.

마주친 문제

아직 없음.

묶음 (이 branch에서 파생된 자료)

관련 일일 노트

해당 없음.

완료 후 정리

  • PR 링크:
  • 리뷰 메모:
  • 머지 결과 / 배포 환경:
  • wiki 추출 대상 (verified만, wiki/projects/로만 추출):
  • 추출하지 않을 항목 (planned / documented-only / abandoned):