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

458 lines
65 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: branch / feature-capability-provider-selection-contract
source_type: branch-note
status: raw
branch: feature-capability-provider-selection-contract
parent_branch:
related_projects: [ca-skeleton]
governing_docs: [raw/project-notes/ca-skeleton-operational-contract]
tags: [branch, ca-skeleton, capability-platform, provider-selection, readiness]
created: 2026-07-28
target_merge:
status_label: in-progress
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-060
kind: project-work-item
project: ca-skeleton-operational-contract
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-060
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-CAPABILITY-SELECTION-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-CAPABILITY-READINESS-001@1]
refines: []
overrides: []
depends_on: []
imports: []
delegates: []
accepts_delegations: []
contract_packet: 1
---
# branch: feature-capability-provider-selection-contract
> Layer: `raw/branch-notes/` — 단일 브랜치의 **TODO·결정·진행 기록**. 머지/종료 후 verified 결과는 `/ingest`로 `wiki/projects/`에 추출. 원본은 raw에 영구 보관.
> `status_label`: `in-progress` | `review` | `merged` | `abandoned`
> **스캐폴딩 상태** — 결정(D-row)·구현 가이드는 비어 있다. `/branch-spec feature-capability-provider-selection-contract` 로 채운다.
<!-- section-id: branch-parent -->
## 부모 (필수)
- **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 P). 본 branch 는 project §8.0 `WI-CA-SKELETON-OPERATIONAL-CONTRACT-060` 의 실행 단위다.
형제 branch (같은 부모의 다른 자식 — 인접 영역):
- [[raw/branch-notes/feature-env-driven-runtime-configuration]]
- [[raw/branch-notes/feature-integration-adapter-templates]]
- [[raw/branch-notes/feature-runtime-health-lifecycle-contract]]
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
> project Work Item 에서 내려온 실행 계약의 snapshot. 여기에는 **pinned pointer + 1줄 요약 + branch 적용점**만 쓰고 상세를 복제하지 않는다.
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: capability descriptor·provider 선택·readiness 등급 검증 test 가 통과하고 bean 이름 기반 판정이 제거된다
<!-- section-id: inherited-project-decisions -->
### 상속한 프로젝트 결정
| 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]] |
<!-- section-id: branch-local-decisions -->
### 브랜치 지역 결정
> 상세 근거와 선택 조건은 아래 `## 결정-근거 매핑`의 동일 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` |
<!-- section-id: declared-overrides -->
### 선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|---|---|---|---|
<!-- GENERATED: project-contract-imports:start -->
## 가져온 프로젝트 계약
| Ref | Owner | 요약 | Branch 적용 |
|---|---|---|---|
<!-- GENERATED: project-contract-imports:end -->
<!-- section-id: branch-goal -->
## 목표
- `WI-CA-SKELETON-OPERATIONAL-CONTRACT-060` 의 완료 조건을 구현한다: capability descriptor·provider 선택·readiness 등급 검증 test 가 통과하고 bean 이름 기반 판정이 제거된다
- 이슈:
- PR:
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- 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`
- [x] `/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) `failureMode``failurePolicy` 잔존 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 가 설정 없이 활성
<!-- section-id: decision-evidence -->
## 결정-근거 매핑
> `Supporting Claims` 의 `raw/...#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-C1``matchIfMissing` **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-C9``management.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-C8``needs-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.yaml``STARTUP_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` 과 같은 미결 항목 |
<!-- section-id: implementation -->
## 구현 가이드
> 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.assumeTrue` 로 **SKIP**(`: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_ENABLED` → `app.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-251` 의 `readiness: 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 반영의 공통 계약만 다룬다.
<!-- section-id: edge-failure-dependency -->
## 엣지·실패·의존
- **실패·엣지 경로**
- 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`) — 로그만이 유일한 진단 수단이다.
- **다른 계약 의존**
- [[raw/branch-notes/feature-env-driven-runtime-configuration]] D2·D7·D8 — `APP_` prefix 통일, `verifyEnvKeys` drift 검증, multi-instance flag. **D8 의 판정 메커니즘은 본 branch 로 이전(H8)**, flag 자체와 registry row 는 env-driven 유지. 그 계약이 바뀌면 D3·D5 가 직접 영향받는다.
- [[raw/branch-notes/feature-contract-registry-governance]] D1·D4·D6 — 신규 registry family 의 스키마와 저장 위치. D11 이 전적으로 의존한다.
- [[raw/branch-notes/feature-migration-startup-contract]] — `STARTUP_VALIDATION_FAILED`·`REQUIRED_ADAPTER_DISABLED`·`PROFILE_MISMATCH` 의 owner. D10 은 소비만 한다.
- [[raw/branch-notes/feature-integration-adapter-templates]] — `ADAPTER_DISABLED` owner. 본 branch 가 활성화 SSOT 를 가져가면 그 branch 는 소비자로 강등된다(설계 §5.3).
- [[raw/branch-notes/feature-runtime-health-lifecycle-contract]] D2·D9·D10 — health endpoint shape 와 required/optional dependency 구분의 owner. D7 은 그 위에 판정 입력만 얹는다.
- [[raw/branch-notes/feature-repository-access-permission-contract]] — `capabilities.yaml``@UseCaseCapability` 의 owner. D1 의 어휘 분리는 이 계약을 **건드리지 않기 위한** 결정이다.
## 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.yaml`**0개** | `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` 에서 정정했다.
<!-- section-id: claims-to-verify -->
## 검증해야 할 주장
| 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-29``havingValue="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에서 파생된 자료)
<!-- GENERATED: branches:start -->
<!-- GENERATED: branches:end -->
## 관련 일일 노트
해당 없음.
## 완료 후 정리
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
- **추출하지 않을 항목** (planned / documented-only / abandoned):