feat(slot): BAT-028 M2/M3 — universe reconcile + 자산 중심 admission (dcy-020/REQ-058·059)

config 게이트 이중 경로(max_active_slots 설정 시 자산 중심, 미설정 시 legacy pool).
운영 데몬은 cutover(BAT-δ) 전까지 pool 무중단.

- repository: find_slot_by_portfolio·count_active(holding+pending_buy+pending_sell)·
  reconcile_universe(생성 멱등/soft-retire/tombstone, allow_retire 가드) — protocol+in_memory+PG
  (PG: ON CONFLICT (instance_key, portfolio_id) WHERE portfolio_id IS NOT NULL)
- snapshot_scheduler: run_once swap 직후 _reconcile_slots(빈 universe skip, ≥50%↓ retire 보류)
- intents: _admit_buy 모드 분기 + _admit_buy_asset_centric(활성화/already_active/
  capacity_full/unknown_portfolio/over-cap, 인스턴스 lock)
- daemon: instance_key·max_active_slots·reconcile_enabled·trading_date_fn 배선
- 신규 테스트: test_universe_reconcile(6), test_asset_centric_admission(6), PG reconcile(1)

검증: 단위 387 passed / 35 skipped, PG-gated 17 passed, ruff clean. legacy 무파손.
REQ-058/059/060 은 Approved 유지(자산 중심 경로 도입 검증; default 전환·cutover 는 BAT-δ).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 11:14:50 +09:00
co-authored by Claude Opus 4.8
parent 587e987c5c
commit 8a377e6fb3
11 changed files with 476 additions and 9 deletions
@@ -1,16 +1,27 @@
# BAT-028 Implementation
- Status: not-started
- Status: completed
- Batch: BAT-028
## 구현 요약
- (run-batch-delivery 진입 시 작성) design.md 체크포인트(D1/D2/D3) 확정 후 채운다.
- config 게이트 이중 경로(D1). `max_active_slots` 설정 시 자산 중심(reconcile + FCFS/M admission), 미설정 시 legacy pool. 운영 데몬은 cutover 전까지 pool 무중단.
## 변경 내역
- (대기)
### M1 — REQ-060 config (max_active_slots)
- `runtime/config.py`: `DaemonInstanceConfig.max_active_slots`/`DaemonConfig.max_active_slots`(Optional[int]) + from_toml(>0 검증) + from_config 전파.
### M2 — REQ-058 universe reconcile
- `domain/slot/repository.py`(protocol)·`in_memory_repository.py`·`adapters/persistence/postgresql.py`: `find_slot_by_portfolio(instance_key, portfolio_id)`, `count_active(instance_key)`(holding+pending_buy+pending_sell, D2), `reconcile_universe(instance_key, trading_date, universe, *, allow_retire=True)` — 신규 자산 생성(멱등; PG ON CONFLICT 부분 unique), 비활성 soft-retire, 보유/pending tombstone. PG insert status='idle'(별칭).
- `application/snapshot_scheduler.py`: `run_once` swap 직후 `_reconcile_slots`(reconcile_enabled 시) — 빈 universe skip, 급감(≥50%↓) 시 allow_retire=False+경고(D3). instance_key/trading_date_fn 주입.
### M3 — REQ-059 자산 중심 admission
- `application/intents.py`: `IntentService(instance_key, max_active_slots)` + `_admit_buy` 모드 분기 + `_admit_buy_asset_centric`(자산 슬롯 활성화, already_active/capacity_full/unknown_portfolio/over-cap, 인스턴스 lock 재시도).
- `runtime/daemon.py`: IntentService/SnapshotScheduler 에 instance_key·max_active_slots·reconcile_enabled·trading_date_fn 배선. `datetime` import 추가.
## 테스트
- (대기)
- 신규: tests/domain/slot/test_universe_reconcile.py(6), tests/application/test_asset_centric_admission.py(6), test_postgresql_repository.py::test_universe_reconcile_pg(PG).
- legacy 회귀: max_active_slots 미설정 시 기존 pool 경로 무파손.
## 검증 실행
- (대기)
- 단위 전체: 387 passed / 35 skipped / 1 deselected. ruff clean.
- PG-gated(trex_core_test): persistence 17 passed(reconcile ON CONFLICT 부분 인덱스 + find_by_portfolio/count_active + soft-retire 포함).
@@ -1,13 +1,44 @@
# BAT-028 Verification
- Status: not-started
- Status: verified (배치 범위)
- Batch: BAT-028
## Acceptance Mapping
- (draft-batch-verification / confirm-batch-verification 단계에서 작성)
### REQ-060 (max_active_slots config)
| AC | 상태 | Evidence |
|----|------|----------|
| TOML 로드 + admission M 사용 | ✅ | config from_toml + test_asset_centric_admission(M 캡) |
| 미설정 → legacy pool | ✅ | test_legacy_pool_mode_unaffected |
| >0 검증 | ✅ | from_toml raise (단위) |
### REQ-058 (universe reconcile)
| AC | 상태 | Evidence |
|----|------|----------|
| 신규 자산 생성(멱등) | ✅ | test_universe_reconcile(in-mem) + test_universe_reconcile_pg(ON CONFLICT) |
| 비활성 soft-retire(행 보존, 재등장 재활성) | ✅ | test_soft_retire_* + PG |
| 보유/pending tombstone(은퇴 보류) | ✅ | test_tombstone_holding_* |
| degrade/대량소멸 보류 | ✅ | snapshot_scheduler `_reconcile_slots`(빈 universe skip, ≥50%↓ allow_retire=False) + test_allow_retire_false |
### REQ-059 (자산 중심 admission)
| AC | 상태 | Evidence |
|----|------|----------|
| 자산 슬롯 활성화(inactive→pending_buy) | ✅ | test_activates_asset_slot |
| capacity_full at M(무치환) | ✅ | test_capacity_full_at_M_no_eviction |
| already_active | ✅ | test_already_active_is_rejected |
| over-cap(신규만 차단·기존 유지) | ✅ | test_over_cap_blocks_new_keeps_existing |
| unknown_portfolio(자산 슬롯 부재) | ✅ | test_unknown_portfolio_when_no_asset_slot |
## Evidence
- (대기)
- 단위 전체: `387 passed / 35 skipped / 1 deselected`(신규 12 포함). ruff clean.
- PG-gated(실 trex_core_test): persistence `17 passed` — reconcile(ON CONFLICT 부분 unique)·find_slot_by_portfolio·count_active·soft-retire + 마이그레이션 009 드리프트.
## Carried-Over
- 라이브 데몬 default 는 legacy pool(max_active_slots 미설정) — 자산 중심 default 전환·컬럼 drop·literal inactive 전면화는 **BAT-δ(cutover)**.
- intent 자산 키 통일(sell-by-portfolio_id)·수량/불일치·운영자 뷰 → **BAT-γ**.
- M 캡 strict 동시성은 단일 intent 스레드 + 인스턴스 advisory lock 전제(production). 실 데몬 자산 중심 e2e 는 BAT-γ/δ run-the-app 에서.
- 따라서 REQ-058/059/060 은 본 배치에서 **Approved 유지**(자산 중심 경로 동작 검증; 전체 dcy-020 완성·default 전환은 후속).
## Result
- Ready for Release: false (구현 전)
- Ready for Release: false (dcy-020 전체 delivery 미완 — β 경로 도입 완료, default 는 legacy)
- Blocking Issues: 없음(단위 387 + PG 17 green, legacy 무파손).