docs(req): dcy-020 confirm + 추가 사양(FR-6~8/NFR-4~5/OQ-8) + draft-req REQ-056~065

dcy-020(자산 중심 영구 슬롯 모델) Requirements phase.

- 추가 사양: FR-6(상태머신 재정의), FR-7(intent 자산 키), FR-8(운영자 단일뷰),
  NFR-4(전략 계약 호환), NFR-5(불일치 감지 주기), OQ-8(over-cap=신규 차단·기존 유지),
  dcy-019 산출물 제거 명시. review-discovery 정리 반영.
- confirm-discovery: 상태 draft→confirmed, Handoff 승인, Ready=true,
  Confirmed By=Justin Ko, 2026-06-10. index 행 confirmed.
- draft-req: 신규 REQ-056~065(전부 Proposed) 생성
  - 056 영구 슬롯 스키마 / 057 상태머신+인스턴스 lock / 058 universe reconcile
  - 059 capacity·admission / 060 max_active_slots TOML / 061 intent 자산 키
  - 062 수량 hub 정본+불일치 해소 / 063 slots 단일뷰 / 064 cutover / 065 baseline
  - srs/index Next ID→066, Register 10행, Recent Change Log 갱신
  - dcy-020 생성된 REQ 참조 갱신

기존 REQ-002/003/005/006/007/048 폐기·재정의 change-req 는 후속(문서 단계).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 10:28:38 +09:00
co-authored by Claude Opus 4.8
parent 1db7ab13bb
commit e2a71e50e2
13 changed files with 676 additions and 24 deletions
@@ -0,0 +1,61 @@
# req-060: 동시 활성 최대치(max_active_slots) per-instance TOML 설정
- Status: Proposed
- Type: Configuration
- Priority: High
- Owner: project maintainers
## Intent
- 동시 활성(holding) 슬롯 최대치 M 을 인스턴스별로 독립 제어하기 위해 per-instance TOML 설정 키로 노출한다. 영구 슬롯 전환으로 폐기되는 `TREX_TOTAL_SLOTS`(풀 크기)를 대체한다. (dcy-020 FR-2/OQ-6)
## Requirement
- `DaemonInstanceConfig`(REQ-048 per-instance TOML)에 `max_active_slots`(정수, M) 키를 추가하고 `config/*.toml` 에서 읽는다. admission(REQ-059)이 이 값을 capacity 게이트로 사용한다. env(`TREX_*`)·하드코딩 기본값은 채택하지 않으며, 기존 `TREX_TOTAL_SLOTS` 는 폐기한다. 미설정 시 부팅 실패 또는 명시적 검증 오류로 처리한다(암묵 기본값 금지).
## Acceptance Criteria
1. Given `config/inst.toml` 에 `max_active_slots = N`, When 데몬 기동, Then `DaemonInstanceConfig.max_active_slots == N` 이고 admission 이 N 을 M 으로 사용한다.
2. Given `max_active_slots` 미설정, When 기동, Then 명시적 오류로 거부된다(암묵 기본값 사용 안 함).
3. Given 두 인스턴스가 서로 다른 `max_active_slots`, When 각자 기동, Then 인스턴스별 독립 M 으로 동작한다.
## Impacted Area
- Docs: docs/data-model.md/docs/runtime-flows.md(설정), config/instance.example.toml
- Modules: trex_core/runtime/config.py(DaemonInstanceConfig)
- Tests: tests/runtime/test_config*.py
## Notes
- REQ-048(per-instance TOML 로더) 계열. `TREX_TOTAL_SLOTS` 폐기는 REQ-005/REQ-010 영향(change-req 검토).
- 출처: dcy-020 FR-2, OQ-6.
## Change Log
### CHG-20260610-01
- Date: 2026-06-10
- Author: Justin Ko
#### Change Summary
- dcy-020 FR-2/OQ-6 로부터 최초 생성(Proposed). `max_active_slots` per-instance TOML + `TREX_TOTAL_SLOTS` 폐기.
#### Intent
- 동시 활성 최대치 M 을 인스턴스별 TOML 로 제어하도록 backlog 정규화.
#### Acceptance Criteria Delta
- Added: TOML 로딩/미설정 거부/인스턴스별 독립 3개 기준
- Updated: 없음
- Removed: 없음
#### Impacted Area Delta
- Docs: config/instance.example.toml, baseline 문서
- Modules: runtime/config.py(DaemonInstanceConfig)
- Tests: test_config*.py
#### Delivery Trace Delta
- Batches: none
- Releases: none
#### Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no
- Status Recommendation: keep-current
- Follow-up Action: confirm-req. REQ-048 계열, REQ-005/010(TREX_TOTAL_SLOTS) change-req 검토.
#### Validation Plan
- TOML 키 로딩 + 미설정 거부 단위테스트 green.
@@ -0,0 +1,64 @@
# req-056: 자산 영구 슬롯 스키마 재설계 + 마이그레이션
- Status: Proposed
- Type: Data
- Priority: High
- Owner: project maintainers
## Intent
- 익명 슬롯 풀(`daily_slot` 일일 익명 행)을 portfolio-hub universe 자산과 1:1 대응하는 **영구 슬롯**으로 전환하기 위해, `daily_slot` 스키마를 자산 키·인스턴스 스코프·영속 모델로 재설계한다. (dcy-020 FR-1/OQ-3/OQ-7)
## Requirement
- `daily_slot` 을 영구 슬롯으로 재설계한다: `portfolio_id` 를 **NOT NULL 영구 키**로, `instance_key` 컬럼 추가, **`UNIQUE(instance_key, portfolio_id)`**, 은퇴 마커 `retired_at`(또는 `universe_detached`) 추가. `trading_date`(슬롯 정체성)·`entry_count`(dcy-019) 컬럼은 제거하고, trading_date 는 이벤트/원장/정책의 시간 속성으로만 둔다. 멱등 마이그레이션(009) + `EXPECTED_SLOT_FIELDS` 계약 + persistence flat 매핑 + `data-model.md` 를 같은 변경 묶음으로 정합한다. slot-scoped 테이블(slot_transition_log/intent_log/ledger_outbox/snapshot_pause/killswitch_exclusion)도 `instance_key` 스코프화한다.
## Acceptance Criteria
1. Given 마이그레이션 적용, When `daily_slot` 스키마를 확인, Then `portfolio_id NOT NULL`, `instance_key` 존재, `UNIQUE(instance_key, portfolio_id)`, `retired_at` 존재, 슬롯 정체성 `trading_date`·`entry_count` 부재.
2. Given `EXPECTED_SLOT_FIELDS` 계약 가드, When 도메인 필드셋과 information_schema 를 비교, Then 영구 슬롯 필드셋과 일치한다.
3. Given 같은 `(instance_key, portfolio_id)` 행 중복 삽입, When insert, Then UNIQUE 제약으로 거부된다.
4. Given slot-scoped 테이블, When 스키마 확인, Then 각 테이블이 `instance_key` 로 스코프되어 인스턴스 간 행이 섞이지 않는다.
## Impacted Area
- Docs: docs/data-model.md (daily_slot 재설계), dcy-020
- Modules: trex_core/adapters/persistence/sql/009_*.sql(신규), trex_core/adapters/persistence/postgresql.py, trex_core/domain/slot/models.py
- Tests: tests/domain/slot/test_contract.py, tests/adapters/persistence/test_migration_and_drift.py
## Notes
- 기존 REQ 영향(change-req 동반): REQ-002(Slot 필드/EXPECTED_SLOT_FIELDS 재정의), REQ-006(persistence 스키마), REQ-003(계약 가드). dcy-019 `entry_count`·dcy-014 `trading_date` 슬롯키 제거는 그 REQ들의 change-req 로 함께 처리.
- 라이브 데이터 일일행→영구행 전환은 REQ-064(cutover)에서 다룬다(본 REQ 는 목표 스키마 정의).
- 출처: dcy-020 FR-1, OQ-3, OQ-7.
## Change Log
### CHG-20260610-01
- Date: 2026-06-10
- Author: Justin Ko
#### Change Summary
- dcy-020 FR-1/OQ-3/OQ-7 로부터 최초 생성(Proposed). 영구 슬롯 스키마(자산 키·instance_key·UNIQUE 복합·retired_at, trading_date/entry_count 제거).
#### Intent
- 익명 풀 daily_slot → 자산 1:1 영구 슬롯 스키마 전환을 backlog 로 정규화.
#### Acceptance Criteria Delta
- Added:
- 영구 슬롯 스키마/UNIQUE(instance_key, portfolio_id)/계약 가드/slot-scoped 테이블 instance 스코프 4개 기준
- Updated: 없음
- Removed: 없음
#### Impacted Area Delta
- Docs: docs/data-model.md
- Modules: sql/009_*, postgresql.py, domain/slot/models.py
- Tests: test_contract.py, test_migration_and_drift.py
#### Delivery Trace Delta
- Batches: none (batch 편성 전)
- Releases: none
#### Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no (목표 스키마 정의; 실데이터 전환은 REQ-064)
- Status Recommendation: keep-current
- Follow-up Action: confirm-req → suggest-batch-reqs/draft-batch. REQ-002/003/006 change-req 동반.
#### Validation Plan
- 마이그레이션 적용 후 스키마/UNIQUE/계약 가드 + drift 테스트 green.
@@ -0,0 +1,61 @@
# req-065: 자산 중심 모델 baseline 문서 정합 + 풀 모델 폐기 명시
- Status: Proposed
- Type: Documentation
- Priority: Medium
- Owner: project maintainers
## Intent
- 자산 중심 영구 슬롯 모델 전환 후 baseline 문서를 실제 구조/흐름/데이터 모델과 일치시키고, 폐기되는 풀 모델(dcy-001/002/014/019) 산출물의 처분을 명시한다. (dcy-020 FR-5)
## Requirement
- `docs/data-model.md`(영구 slot 스키마·instance_key·retired_at·trading_date 강등·entry_count 제거), `docs/runtime-flows.md`(universe reconcile lifecycle·신호 기반 admission·불일치 해소·일일 preallocation 폐기), `docs/project-structure.md`(자산 중심 슬롯 모델)를 새 모델로 갱신한다. dcy-001(6상태/closed→idle)·dcy-014(일일 preallocation)·dcy-019(entry_count/closed 재할당/find_allocatable_slot) 의 폐기/재해석 범위와 `daily_symbol_entries` 폐기를 명시한다.
## Acceptance Criteria
1. Given 전환 구현 완료, When `docs/data-model.md` 확인, Then 영구 슬롯 스키마·상태·트레이딩데이트 강등이 코드와 일치한다.
2. Given 전환 구현 완료, When `docs/runtime-flows.md` 확인, Then reconcile lifecycle·admission·불일치 해소 흐름이 실제와 일치하고 일일 preallocation 서술이 제거된다.
3. Given 폐기 대상, When 문서 확인, Then dcy-001/014/019·daily_symbol_entries 폐기/재해석 범위가 명시된다.
## Impacted Area
- Docs: docs/data-model.md, docs/runtime-flows.md, docs/project-structure.md
- Modules: 없음(문서 전용)
- Tests: 없음(코드-문서 diff 대조)
## Notes
- 다른 dcy-020 REQ 구현 완료 후 수행해야 의미가 있다(순서 의존). baseline 갱신은 각 배치의 일부로 분산 가능.
- 출처: dcy-020 FR-5.
## Change Log
### CHG-20260610-01
- Date: 2026-06-10
- Author: Justin Ko
#### Change Summary
- dcy-020 FR-5 로부터 최초 생성(Proposed). baseline 3문서 자산 중심 재설계 + dcy-001/014/019·daily_symbol_entries 폐기 명시.
#### Intent
- 모델 전환 후 baseline 문서 정합과 폐기 범위 명시를 backlog 정규화.
#### Acceptance Criteria Delta
- Added: data-model/runtime-flows/폐기 명시 3개 기준
- Updated: 없음
- Removed: 없음
#### Impacted Area Delta
- Docs: docs/data-model.md, docs/runtime-flows.md, docs/project-structure.md
- Modules: 없음(문서 전용)
- Tests: 없음(코드-문서 diff)
#### Delivery Trace Delta
- Batches: none
- Releases: none
#### Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no
- Status Recommendation: keep-current
- Follow-up Action: confirm-req. 다른 dcy-020 REQ 구현 후 수행(순서 의존).
#### Validation Plan
- 코드-문서 diff 대조로 정합 확인.
@@ -0,0 +1,63 @@
# req-058: universe 기반 슬롯 lifecycle reconcile (생성/soft-retire/tombstone)
- Status: Proposed
- Type: Integration
- Priority: High
- Owner: project maintainers
## Intent
- 슬롯 집합을 portfolio-hub universe 의 projection 으로 유지하기 위해, universe fetch 결과로 슬롯을 생성/은퇴하는 reconcile 경로를 정의한다. 일일 preallocation(dcy-014)을 폐기하고 universe 동기화로 대체한다. (dcy-020 FR-1/OQ-2/NFR-3)
## Requirement
- 스냅샷 refresh(`get_portfolios` 성공 직후)에 슬롯 reconcile 을 수행한다: (1) universe 신규 자산 → 영구 슬롯 즉시 생성(비활성·멱등, `UNIQUE(instance_key, portfolio_id)`); (2) universe 제거 + 슬롯 비활성(holding/pending 아님) → **soft-retire**(`retired_at` 마킹, 행 보존, hard delete 금지); (3) universe 제거 + 보유 중/pending → **tombstone**(은퇴 보류, safety-sell/watchdog 유지, 신규 매수 후보 제외, 닫힌 뒤 은퇴). reconcile 쓰기는 인스턴스 스코프 advisory lock 안에서 단일 writer 로 수행한다. cadence 는 기존 부팅 + 일 1회 + 운영자 on-demand. portfolio-hub 불가/degraded 또는 universe N→0/급격 대량 감소면 생성·은퇴를 보류하고 경고한다.
## Acceptance Criteria
1. Given universe 에 신규 자산, When reconcile, Then 그 자산의 영구 슬롯이 비활성으로 생성되고 재실행해도 중복 생성되지 않는다(멱등).
2. Given universe 에서 빠진 비활성 슬롯, When reconcile, Then soft-retire(`retired_at` set, 행 보존)되고, 같은 자산이 재등장하면 동일 슬롯이 재활성화된다.
3. Given universe 에서 빠졌으나 holding/pending 인 슬롯, When reconcile, Then 은퇴되지 않고 tombstone(신규 매수 제외·safety-sell 유지)되며, 매도 체결 후에야 은퇴된다.
4. Given portfolio-hub degraded 또는 universe 가 N→0, When reconcile, Then 슬롯 생성·은퇴가 보류되고 마지막 집합이 유지되며 경고가 남는다.
## Impacted Area
- Docs: docs/runtime-flows.md(reconcile lifecycle)
- Modules: trex_core/application/snapshot_scheduler.py, trex_core/domain/slot/repository.py, trex_core/adapters/persistence/postgresql.py, trex_core/application/preallocation_scheduler.py(폐기)
- Tests: tests/application/test_snapshot_scheduler.py(reconcile), tests/application/test_preallocation_scheduler.py(폐기/대체)
## Notes
- 기존 REQ-005/REQ-048(일일 preallocation·instance config) change-req 동반(preallocation 폐기·재해석). trading_date KST 계산(REQ-005)은 일일 회계 경계용으로 부분 존속.
- 출처: dcy-020 FR-1, OQ-2, NFR-3.
## Change Log
### CHG-20260610-01
- Date: 2026-06-10
- Author: Justin Ko
#### Change Summary
- dcy-020 FR-1/OQ-2/NFR-3 로부터 최초 생성(Proposed). universe reconcile(생성/soft-retire/tombstone) + degrade·대량소멸 보류 + 일일 preallocation 폐기.
#### Intent
- 슬롯 집합을 universe projection 으로 유지하는 lifecycle 을 backlog 로 정규화.
#### Acceptance Criteria Delta
- Added:
- 생성 멱등/비활성 soft-retire 재활성/tombstone/degrade·대량소멸 보류 4개 기준
- Updated: 없음
- Removed: 없음
#### Impacted Area Delta
- Docs: docs/runtime-flows.md
- Modules: snapshot_scheduler.py, repository.py, postgresql.py, preallocation_scheduler.py(폐기)
- Tests: test_snapshot_scheduler.py, test_preallocation_scheduler.py
#### Delivery Trace Delta
- Batches: none
- Releases: none
#### Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no
- Status Recommendation: keep-current
- Follow-up Action: confirm-req. REQ-005/048 change-req(preallocation 폐기) 동반.
#### Validation Plan
- reconcile 생성/은퇴/tombstone/degrade 단위·통합 테스트 green.
@@ -0,0 +1,62 @@
# req-062: 수량 hub 정본+로컬 미러 + holding↔broker/hub 불일치 해소
- Status: Proposed
- Type: Integration
- Priority: High
- Owner: project maintainers
## Intent
- 보유 수량의 정본을 portfolio-hub 로 두고 슬롯은 미러만 유지하며, 코어 로컬 holding 과 브로커/hub 가 비정상적으로 어긋날 때의 해소 정책을 정의한다. (dcy-020 FR-4/OQ-5/NFR-5)
## Requirement
- 보유 **수량(asset_quantity)의 정본은 portfolio-hub** 이며, 슬롯은 수량을 정본 소유하지 않고 로컬 best-effort 미러(option D write-back/추정)로 둔다. 실제 청산 시 매도 수량은 **브로커 잔고(`get_balance().hldg_qty`)를 정본**으로 조회한다. 불일치 감지는 `reconcile_positions`(REQ-031)를 확장해 **브로커 잔고 + hub 수량 + 코어 슬롯 holding** 3자를 비교하며, 브로커를 ground truth 로 한다. 해소: A(코어 holding/브로커 0) → 슬롯 `error` + 운영자 resolve; B(코어 flat/브로커 보유, 최위험) → WARN/alert + 격리 + 운영자 결정(자동 청산·자동 편입 금지); C(수량만 불일치) → 미러 갱신 + warning, 상태 변경 없음; D(hub 만 지연) → 기존 write-back. **조용한 자동 교정 금지·표면화**, 실주문은 운영자 게이트(issue #15). 감지 reconcile 은 일일 refresh 와 분리해 더 자주 수행 가능(브로커 잔고+슬롯 상태만 필요).
## Acceptance Criteria
1. Given 매도 청산, When 매도 수량 결정, Then 로컬 미러가 stale 해도 브로커 잔고 기준으로 정확히 청산된다.
2. Given 코어 holding 인데 브로커 0, When reconcile, Then 슬롯이 `error` 로 격리되고 safety-sell 대상에서 빠지며 운영자 resolve 가 요구된다.
3. Given 코어 flat 인데 브로커 보유, When reconcile, Then WARN/alert + 격리되고 자동 청산·자동 편입 없이 운영자 결정으로 넘어간다.
4. Given 둘 다 보유, 수량만 불일치, When reconcile, Then 미러만 갱신되고 상태 전이는 없으며 hub 지속 불일치는 warning 으로 노출된다.
## Impacted Area
- Docs: docs/runtime-flows.md(불일치 해소·write-back)
- Modules: trex_core/application/reconciliation.py, trex_core/application/ledger_outbox.py, trex_core/domain/slot/models.py(qty 미러 의미)
- Tests: tests/application/test_reconciliation.py, tests/application/test_ledger_option_d.py
## Notes
- 기존 REQ-017/019/029/030/031 와 연계(option D write-back 재사용). 슬롯 qty 정본 소유 제거는 REQ-002/006 change-req 와 정합.
- 출처: dcy-020 FR-4, OQ-5, NFR-5.
## Change Log
### CHG-20260610-01
- Date: 2026-06-10
- Author: Justin Ko
#### Change Summary
- dcy-020 FR-4/OQ-5/NFR-5 로부터 최초 생성(Proposed). 수량 hub 정본+미러·청산=브로커·불일치 해소(A/B/C/D, 표면화·운영자 게이트).
#### Intent
- 수량 정본 분리와 holding↔broker/hub 불일치 해소 정책을 backlog 정규화.
#### Acceptance Criteria Delta
- Added: 청산=브로커/A error/B 격리·운영자/C 미러갱신 4개 기준
- Updated: 없음
- Removed: 없음
#### Impacted Area Delta
- Docs: docs/runtime-flows.md
- Modules: application/reconciliation.py, ledger_outbox.py, domain/slot/models.py
- Tests: test_reconciliation.py, test_ledger_option_d.py
#### Delivery Trace Delta
- Batches: none
- Releases: none
#### Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no (REQ-031/017/019 연계·확장)
- Status Recommendation: keep-current
- Follow-up Action: confirm-req. REQ-002/006(qty 미러) change-req 정합.
#### Validation Plan
- 청산 브로커 기준 + A/B/C 불일치 해소 단위·통합 테스트 green.
@@ -0,0 +1,64 @@
# req-057: 자산 영구 슬롯 상태머신 재정의 + 인스턴스 스코프 advisory lock
- Status: Proposed
- Type: Interface
- Priority: High
- Owner: project maintainers
## Intent
- 영구 자산 슬롯에 맞춰 슬롯 상태머신을 재정의하고, holding 을 코어 로컬 정본으로 두며, 공유 DB 다중 인스턴스에서 단일 writer 불변식을 인스턴스 단위로 보존한다. (dcy-020 FR-6/FR-3/NFR-1)
## Requirement
- 상태 집합을 `inactive → pending_buy → holding → pending_sell → inactive` (+ 모든 비-error → `error`, `error → {inactive, holding}` 운영자 resolve)로 재정의한다. 기존 `idle`·`closed` 는 영구 슬롯에서 단일 `inactive` 로 합치고 `closed→idle` 복구 전이를 제거한다. `holding`(포지션 열림)은 코어 슬롯의 로컬 정본으로 체결 시 동기 set, 매도 체결 시 동기 clear 한다(portfolio-hub `asset_quantity` 로 판단하지 않음). advisory lock 을 **인스턴스 스코프**(`pg_advisory_xact_lock(SLOT_LOCK_ID, hash(instance_key))`)로 두어 인스턴스 간 교차 직렬화를 제거한다.
## Acceptance Criteria
1. Given 상태머신, When 허용 전이 집합을 확인, Then `{inactive→pending_buy, pending_buy→holding, pending_buy→inactive, holding→pending_sell, pending_sell→inactive, pending_sell→holding, *→error, error→inactive, error→holding}` 와 정확히 일치한다.
2. Given 영구 슬롯, When 매도 체결로 종료, Then 슬롯은 `inactive` 로 돌아가고(별도 `closed`·`closed→idle` 없음) holding 로컬 플래그가 clear 된다.
3. Given 서로 다른 instance_key 의 두 슬롯, When 동시에 전이, Then 인스턴스별 락으로 서로를 차단하지 않는다(교차 경합 없음).
4. Given 같은 슬롯에 대한 동시 전이, When 병렬 처리, Then 인스턴스 스코프 락으로 직렬화되어 단일 writer 불변식이 유지된다.
## Impacted Area
- Docs: docs/runtime-flows.md(전이 흐름), docs/data-model.md(상태)
- Modules: trex_core/domain/slot/state.py, trex_core/domain/slot/service.py, trex_core/domain/slot/models.py
- Tests: tests/domain/slot/test_state.py, tests/domain/slot/test_service.py
## Notes
- 기존 REQ-002(상태머신·서비스) 재정의 change-req 동반. dcy-001 의 6상태/closed→idle 모델 폐기.
- holding 로컬 정본 근거(offline 안전·universe 누락 생존·hub 지연·capacity 원자성)는 dcy-020 FR-3.
- 출처: dcy-020 FR-6, FR-3, NFR-1, OQ-7.
## Change Log
### CHG-20260610-01
- Date: 2026-06-10
- Author: Justin Ko
#### Change Summary
- dcy-020 FR-6/FR-3/NFR-1 로부터 최초 생성(Proposed). 상태머신 재정의(idle/closed→inactive, closed→idle 제거) + holding 로컬 정본 + 인스턴스 스코프 advisory lock.
#### Intent
- 영구 자산 슬롯 상태머신·holding 로컬·인스턴스 단위 단일 writer 를 backlog 로 정규화.
#### Acceptance Criteria Delta
- Added:
- 전이 집합/inactive 종료/인스턴스별 락 비교차/동일 슬롯 직렬화 4개 기준
- Updated: 없음
- Removed: 없음
#### Impacted Area Delta
- Docs: docs/runtime-flows.md, docs/data-model.md
- Modules: domain/slot/state.py, service.py, models.py
- Tests: test_state.py, test_service.py
#### Delivery Trace Delta
- Batches: none
- Releases: none
#### Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no (신규 정의; REQ-002 재정의 change-req 로 기존 폐기)
- Status Recommendation: keep-current
- Follow-up Action: confirm-req. REQ-002 change-req 동반.
#### Validation Plan
- 전이 허용/거부 집합 + 인스턴스 락 단위테스트 green.
@@ -0,0 +1,63 @@
# req-059: 활성 capacity 제어 + 신호 기반 admission (FCFS·M캡·무치환·over-cap)
- Status: Proposed
- Type: Interface
- Priority: High
- Owner: project maintainers
## Intent
- 익명 슬롯 배정 대신 "동시 활성(holding) 최대 M개" 정책으로 capacity 를 제어하고, 매수/매도 신호를 해당 자산 슬롯의 활성화/비활성화로 처리한다. (dcy-020 FR-2/FR-3/OQ-1/OQ-8)
## Requirement
- 매수 신호는 해당 자산 슬롯을 활성화(`inactive→pending_buy`)한다. 활성(holding+pending_buy) 수 < M 이면 즉시 활성화, == M 이면 **거부(`capacity_full`)** 한다. 코어는 자리를 만들려고 기존 holding 을 **자율 청산(치환)하지 않는다**(FCFS·무치환). 동시 후보 tie-break 은 `updated_at` asc → `portfolio_id`(alpha 비사용). M 을 현재 보유 수보다 작게 바꿔 over-cap 이 되면 **신규 활성화만 차단**하고 기존 보유는 자연 종료까지 유지한다(강제 청산 없음). 마지막 슬롯 경합은 인스턴스 스코프 advisory lock + 재시도로 직렬화한다.
## Acceptance Criteria
1. Given 활성 수 < M, When 자산 매수 신호, Then 그 자산 슬롯이 `pending_buy` 로 활성화된다.
2. Given 활성 수 == M, When 새 자산 매수 신호, Then `capacity_full` 로 거부되고 기존 holding 은 청산되지 않는다.
3. Given 이미 활성(pending_buy/holding/pending_sell)인 자산, When 또 매수 신호, Then 멱등 거부(`already_active`)된다.
4. Given M 을 현재 보유 수보다 작게 변경(over-cap), When 신규 매수 신호, Then 활성 수가 M 이하로 내려갈 때까지 신규만 거부되고 기존 보유는 유지된다.
5. Given 동시 매수 신호 경합, When 병렬 처리, Then 인스턴스 스코프 락으로 직렬화되어 M 초과 활성화가 발생하지 않는다.
## Impacted Area
- Docs: docs/runtime-flows.md(admission→활성화 흐름)
- Modules: trex_core/application/intents.py, trex_core/application/portfolio.py(capacity 개념)
- Tests: tests/application/test_intent_concurrency.py, tests/application/test_*admission*.py
## Notes
- M 값 출처는 REQ-060(per-instance TOML). 기존 REQ-007(admission) 재정의 change-req 동반(find_allocatable_slot/idle 배정 폐기).
- 출처: dcy-020 FR-2, FR-3, OQ-1, OQ-8.
## Change Log
### CHG-20260610-01
- Date: 2026-06-10
- Author: Justin Ko
#### Change Summary
- dcy-020 FR-2/FR-3/OQ-1/OQ-8 로부터 최초 생성(Proposed). FCFS + M 캡 + capacity_full 거부 + 무치환 + over-cap(신규만 차단) + already_active.
#### Intent
- 신호 기반 활성화/비활성화와 동시 활성 M 제어를 backlog 로 정규화.
#### Acceptance Criteria Delta
- Added: 활성화/거부/already_active/over-cap/경합 직렬화 5개 기준
- Updated: 없음
- Removed: 없음
#### Impacted Area Delta
- Docs: docs/runtime-flows.md
- Modules: application/intents.py, portfolio.py
- Tests: test_intent_concurrency.py, admission 테스트
#### Delivery Trace Delta
- Batches: none
- Releases: none
#### Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no
- Status Recommendation: keep-current
- Follow-up Action: confirm-req. REQ-007 재정의 change-req·REQ-060(M) 의존.
#### Validation Plan
- FCFS/거부/over-cap/경합 단위·통합 테스트 green.
@@ -0,0 +1,62 @@
# req-061: intent 계약 자산(portfolio_id) 키 통일 + 중복/무효 규칙
- Status: Proposed
- Type: Interface
- Priority: High
- Owner: project maintainers
## Intent
- 자산 중심 모델에서 매수·매도 신호를 모두 자산(portfolio_id)으로 식별하도록 intent 계약을 통일하고, 슬롯이 자산 1:1 영구이므로 `sell-by-slot_id` 노출을 폐기한다. (dcy-020 FR-7/NFR-4)
## Requirement
- 매수·매도 intent 를 모두 `portfolio_id`(자산) 키로 식별한다. 매수 = 해당 자산 슬롯 활성화(`inactive→pending_buy`), 매도 = 비활성화(`holding→pending_sell`). **`sell-by-slot_id` 는 폐기**한다. 중복/무효 신호 규칙: 이미 활성(`pending_buy`/`holding`/`pending_sell`)인 자산에 매수 신호 → 멱등 거부(`already_active`); 보유 아닌 자산에 매도 신호 → 거부(`not_holding`). `intent_id` 멱등(intent_log)은 보존한다. 계약 변경은 외부 전략 모듈을 깨뜨리므로 cutover/버전 절차를 명시한다(NFR-4).
## Acceptance Criteria
1. Given 매도 신호, When `portfolio_id` 로 제출, Then 해당 자산 슬롯이 `pending_sell` 로 전이하고 `slot_id` 없이 처리된다.
2. Given 이미 holding 인 자산, When 매수 신호, Then `already_active` 로 멱등 거부된다.
3. Given 보유하지 않은 자산, When 매도 신호, Then `not_holding` 으로 거부된다.
4. Given 같은 `intent_id` 재전송, When 처리, Then 슬롯 중복 전이 없이 최초 결정과 동일 응답(멱등).
## Impacted Area
- Docs: docs/runtime-flows.md(intent 흐름), docs/interface-contract.md(메시지 계약)
- Modules: trex_core/application/intents.py, 메시지 핸들러/스키마
- Tests: tests/application/test_messaging_intent_consumer.py, tests/application/test_*intent*.py
## Notes
- 기존 REQ-007(intent API) 재정의 change-req 동반(sell-by-slot_id 폐기·자산 키 통일). NFR-4(전략 계약 호환): 외부 전략 모듈 동시 전환 또는 한시적 하위호환 — REQ/배치 cutover 에서 명시.
- 출처: dcy-020 FR-7, NFR-4.
## Change Log
### CHG-20260610-01
- Date: 2026-06-10
- Author: Justin Ko
#### Change Summary
- dcy-020 FR-7/NFR-4 로부터 최초 생성(Proposed). intent 자산(portfolio_id) 키 통일 + sell-by-slot_id 폐기 + already_active/not_holding.
#### Intent
- 매수·매도 신호를 자산 키로 통일하는 계약 변경을 backlog 정규화.
#### Acceptance Criteria Delta
- Added: 매도 자산키/already_active/not_holding/intent_id 멱등 4개 기준
- Updated: 없음
- Removed: 없음
#### Impacted Area Delta
- Docs: docs/runtime-flows.md, docs/interface-contract.md
- Modules: application/intents.py, 메시지 핸들러/스키마
- Tests: test_messaging_intent_consumer.py, intent 테스트
#### Delivery Trace Delta
- Batches: none
- Releases: none
#### Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no (REQ-007 재정의 change-req 로 기존 폐기)
- Status Recommendation: keep-current
- Follow-up Action: confirm-req. NFR-4 외부 전략 계약 cutover 명시.
#### Validation Plan
- 자산 키 매도/거부 규칙/멱등 단위·통합 테스트 green.
@@ -0,0 +1,61 @@
# req-063: 운영자 표면 단일화 — slots = 자산 포지션 단일 뷰
- Status: Proposed
- Type: Interface
- Priority: Medium
- Owner: project maintainers
## Intent
- universe(자산 리스트)와 slots(슬롯 상태)의 이중 표면(BAT-022 혼동 원인)을 해소하기 위해, 슬롯=자산 1:1 모델에서 `trex-core slots` 가 자산 포지션을 단일 뷰로 제공한다. (dcy-020 FR-8)
## Requirement
- `trex-core slots` 가 자산 슬롯(슬롯=자산)을 단일 뷰로 표시한다: `portfolio_id`/symbol/status(inactive/pending_buy/holding/pending_sell/error)/holding 수량 미러/`universe_detached`(tombstone)/`retired`. universe(자산 리스트)와 slots(자산 포지션)는 같은 자산 집합 위의 view 가 되며, 데몬 RPC(스냅샷)로 통일한다(직접 DB 쓰기 없음). 운영자는 자산별 포지션·활성 여부·tombstone 여부를 한 표면에서 본다.
## Acceptance Criteria
1. Given universe N개 자산, When `trex-core slots`, Then N개 자산 슬롯이 status·holding·tombstone 표시와 함께 단일 뷰로 출력된다.
2. Given tombstone(universe-detached holding) 슬롯, When `slots`, Then `universe_detached` 로 구분 표시된다.
3. Given `--status` 필터, When 조회, Then 자산 슬롯 status 로 필터된다.
## Impacted Area
- Docs: docs/runtime-flows.md, docs/interface-contract.md(snapshot envelope slots[])
- Modules: trex_core/runtime/cli.py, snapshot envelope 빌더
- Tests: tests/application/test_messaging_snapshot_consumer.py, CLI 테스트
## Notes
- 기존 REQ-032(운영자 CLI)·REQ-028/029(snapshot envelope) 와 연계 change-req. BAT-022 슬롯 가시화의 자산 중심 재정의.
- 출처: dcy-020 FR-8.
## Change Log
### CHG-20260610-01
- Date: 2026-06-10
- Author: Justin Ko
#### Change Summary
- dcy-020 FR-8 로부터 최초 생성(Proposed). `trex-core slots` = 자산 포지션 단일 뷰(universe↔slots 이중 표면 해소).
#### Intent
- 운영 관측 단일화(자산별 포지션·활성·tombstone)를 backlog 정규화.
#### Acceptance Criteria Delta
- Added: 단일 뷰 출력/tombstone 표시/status 필터 3개 기준
- Updated: 없음
- Removed: 없음
#### Impacted Area Delta
- Docs: docs/runtime-flows.md, docs/interface-contract.md
- Modules: runtime/cli.py, snapshot envelope 빌더
- Tests: test_messaging_snapshot_consumer.py, CLI 테스트
#### Delivery Trace Delta
- Batches: none
- Releases: none
#### Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no (REQ-032/028/029 연계 change-req)
- Status Recommendation: keep-current
- Follow-up Action: confirm-req.
#### Validation Plan
- slots 단일 뷰 출력·tombstone·필터 단위테스트 green.
@@ -0,0 +1,62 @@
# req-064: 풀 모델 → 영구 자산 슬롯 cutover 마이그레이션
- Status: Proposed
- Type: Migration
- Priority: High
- Owner: project maintainers
## Intent
- 출시·운영 중인 익명 풀 모델(일일 daily_slot 행) 데이터를 자산 영구 슬롯 모델로 안전하게 이행한다. 라이브 트레이딩 데이터(보유 포지션)를 잃지 않고 cutover 한다. (dcy-020 NFR-2/R-1)
## Requirement
- 기존 일일 `daily_slot` 행을 **자산당 영구 1행**으로 전환하고, 현재 보유(holding) 포지션을 새 영구 슬롯으로 **carry-over** 한다. 기존 slot-scoped 행(slot_transition_log/intent_log/ledger_outbox 등)에 `instance_key` 를 태깅한다. cutover 는 거래 세션 외 시간에 수행하고, 롤백 계획(역마이그레이션 또는 스냅샷 복원)을 갖춘다. 단계적 전환(읽기 정합 → 모델 전환)을 우선 검토하고, cutover 전후 보유 포지션·safety-sell 보호가 끊기지 않음을 검증한다.
## Acceptance Criteria
1. Given 기존 풀 모델 데이터(보유 N개 포함), When cutover 적용, Then 각 보유 자산이 영구 슬롯 1행(holding)으로 보존되고 수량/임계값 등 보호 컨텍스트가 유실되지 않는다.
2. Given cutover 후, When 데몬 기동, Then universe reconcile(REQ-058)·admission(REQ-059)·safety-sell 이 회귀 없이 동작한다.
3. Given cutover 실패, When 롤백, Then 직전 released 리비전/스키마로 복원 가능하다(롤백 계획 검증).
4. Given slot-scoped 기존 행, When cutover, Then `instance_key` 태깅이 적용되어 인스턴스 스코프 쿼리가 정확하다.
## Impacted Area
- Docs: docs/runtime-flows.md(cutover 절차), 릴리즈 Rollback Plan
- Modules: trex_core/adapters/persistence/sql/(cutover 마이그레이션), 이행 스크립트
- Tests: tests/adapters/persistence/(cutover round-trip), 회귀 스위트
## Notes
- 라이브 데이터 영향 — 배포 게이트·운영자 입회 권장. 실행은 별도 배치(BAT-δ)로 분리하고 confirm-release 전 run-the-app/cutover 리허설 evidence 필요.
- 출처: dcy-020 NFR-2, R-1.
## Change Log
### CHG-20260610-01
- Date: 2026-06-10
- Author: Justin Ko
#### Change Summary
- dcy-020 NFR-2/R-1 로부터 최초 생성(Proposed). 일일행→영구행 cutover + 보유 carry-over + instance_key 태깅 + 롤백 계획.
#### Intent
- 라이브 풀 모델 데이터를 무손실로 영구 슬롯 모델로 이행하는 cutover 를 backlog 정규화.
#### Acceptance Criteria Delta
- Added: 보유 carry-over/기동 회귀/롤백/instance_key 태깅 4개 기준
- Updated: 없음
- Removed: 없음
#### Impacted Area Delta
- Docs: docs/runtime-flows.md, 릴리즈 Rollback Plan
- Modules: persistence cutover 마이그레이션, 이행 스크립트
- Tests: cutover round-trip, 회귀 스위트
#### Delivery Trace Delta
- Batches: none
- Releases: none
#### Revalidation Impact
- Reverification Needed: yes (cutover 리허설/회귀 필수)
- Existing Implementation Invalidated: no (이행 절차 신규)
- Status Recommendation: keep-current
- Follow-up Action: confirm-req. 별도 배치(BAT-δ)·운영자 입회·confirm-release 전 리허설 evidence.
#### Validation Plan
- cutover 리허설(보유 보존·기동 회귀·롤백) + run-the-app evidence.
+12 -2
View File
@@ -74,7 +74,7 @@
## Next Requirement ID
- Current: `REQ-056`
- Current: `REQ-066`
- Rule: `Requirement Register`에 있는 가장 큰 REQ 번호보다 정확히 1 커야 한다.
## Requirement Register
@@ -136,11 +136,21 @@
| REQ-053 | safety-sell 재시도 쿨다운 env 설정 2종 | Configuration | Implemented | Medium | project maintainers | [req-053](./Configuration/req-053_safety-sell-retry-cooldown-env.md) |
| REQ-054 | safety-sell 쿨다운 runtime-flows 정합 | Documentation | Implemented | Low | project maintainers | [req-054](./Documentation/req-054_safety-sell-cooldown-baseline-alignment.md) |
| REQ-055 | closed 슬롯 ↔ ledger_outbox 누락 운영 점검 쿼리 | Exception | Implemented | Medium | project maintainers | [req-055](./Exception/req-055_ledger-gap-ops-check.md) |
| REQ-056 | 자산 영구 슬롯 스키마 재설계 + 마이그레이션 | Data | Proposed | High | project maintainers | [req-056](./Data/req-056_asset-permanent-slot-schema.md) |
| REQ-057 | 자산 영구 슬롯 상태머신 재정의 + 인스턴스 스코프 advisory lock | Interface | Proposed | High | project maintainers | [req-057](./Interface/req-057_asset-slot-state-machine.md) |
| REQ-058 | universe 기반 슬롯 lifecycle reconcile (생성/soft-retire/tombstone) | Integration | Proposed | High | project maintainers | [req-058](./Integration/req-058_universe-slot-reconcile-lifecycle.md) |
| REQ-059 | 활성 capacity 제어 + 신호 기반 admission (FCFS·M캡·무치환·over-cap) | Interface | Proposed | High | project maintainers | [req-059](./Interface/req-059_active-capacity-admission.md) |
| REQ-060 | 동시 활성 최대치(max_active_slots) per-instance TOML 설정 | Configuration | Proposed | High | project maintainers | [req-060](./Configuration/req-060_max-active-slots-config.md) |
| REQ-061 | intent 계약 자산(portfolio_id) 키 통일 + 중복/무효 규칙 | Interface | Proposed | High | project maintainers | [req-061](./Interface/req-061_asset-keyed-intent-contract.md) |
| REQ-062 | 수량 hub 정본+로컬 미러 + holding↔broker/hub 불일치 해소 | Integration | Proposed | High | project maintainers | [req-062](./Integration/req-062_quantity-hub-authority-and-divergence.md) |
| REQ-063 | 운영자 표면 단일화 — slots = 자산 포지션 단일 뷰 | Interface | Proposed | Medium | project maintainers | [req-063](./Interface/req-063_unified-operator-slot-view.md) |
| REQ-064 | 풀 모델 → 영구 자산 슬롯 cutover 마이그레이션 | Migration | Proposed | High | project maintainers | [req-064](./Migration/req-064_pool-to-permanent-slot-cutover.md) |
| REQ-065 | 자산 중심 모델 baseline 문서 정합 + 풀 모델 폐기 명시 | Documentation | Proposed | Medium | project maintainers | [req-065](./Documentation/req-065_asset-centric-baseline-alignment.md) |
## Recent Change Log Summary
- 2026-06-10 :: REQ-056~065 신규 생성(Proposed) (draft-req, dcy-020 자산 중심 슬롯 모델 재설계): 익명 슬롯 풀 → portfolio-hub universe 자산과 1:1 **영구 슬롯** + 동시 활성(M) 제어로 substrate 전환. REQ-056(영구 슬롯 스키마·UNIQUE(instance_key,portfolio_id)·trading_date/entry_count 제거), REQ-057(상태머신 inactive↔pending_buy↔holding↔pending_sell·holding 로컬·인스턴스 스코프 lock), REQ-058(universe reconcile 생성/soft-retire/tombstone·degrade 보류·일일 preallocation 폐기), REQ-059(FCFS·M캡·무치환·over-cap), REQ-060(max_active_slots per-instance TOML), REQ-061(intent 자산 키 통일·sell-by-slot_id 폐기), REQ-062(수량 hub 정본+미러·청산 브로커·불일치 해소), REQ-063(slots 단일 뷰), REQ-064(풀→영구 cutover), REQ-065(baseline 정합·폐기 명시). 기존 REQ-002/003/005/006/007/048 등은 폐기/재정의 change-req 동반 예정. 단계적 배치(스키마·상태머신 → reconcile·admission → intent·수량·CLI → cutover) delivery. 전부 **Proposed**(confirm-req 대기). (draft-req)
- 2026-06-09 :: REQ-048/010 change-req→Implemented (CHG-20260609-01/02, trex-orb-v1 설정 관리 정합, BAT-026): config 디렉터리 기반 설정 관리 도입 — `--config` 를 이름으로 해석 + `--config-path <dir>`(기본 `./config`) + `resolve_config_toml`(슬래시 포함 값은 하위호환 명시 경로)(REQ-048), `.env` 를 config-dir 결정적 해석(`--env-file` 우선, 없으면 `<config_dir>/.env`, CWD 무관)으로 변경 + `config/.env.example` + `.gitignore` `config/*` 허용목록(REQ-010). 운영자 CLI 도 `--config-path`/`--env-file` 동일 적용. `from_env()` legacy 루트 `.env` 폴백 유지로 완전 하위호환(`.env` 이동은 운영 배포 시 무중단). batch-lite(BAT-026) 단위 23 + 전체 375 passed, ruff clean, 라이브 스모크(name·path 동일 결과) → 둘 다 **Implemented**. (change-req/confirm-req-implemented)
- 2026-06-09 :: REQ-002/003/006/007/009 change-req (CHG-20260609-01, dcy-019/issue #18, BAT-025): slot-scoped `entry_count` 추가 + closed 슬롯 직접 재할당. daily_slot/`Slot` 에 `entry_count`(기본 0) 필드 + `EXPECTED_SLOT_FIELDS` + 멱등 마이그레이션 008(REQ-002/006), 상태 머신 `closed→pending_buy` 신규 전이(8→9 base, ALLOWED_TRANSITIONS 14→15)(REQ-002), `_apply_transition_fields` closed→pending_buy clean-slate + 모든 `→pending_buy` 시 entry_count +1(REQ-002), `find_allocatable_slot`(idle∪closed, `entry_count→updated_at→slot_id`) selector + in-memory 동등(REQ-006), `_admit_buy` find_idle_slot→find_allocatable_slot + from_state=slot.status(REQ-007), 테스트 계약 정합(REQ-003), baseline 2문서(data-model/runtime-flows) 정합(REQ-009). 기존 `daily_symbol_entries`(symbol-scoped) 무관·미변경. 전부 change-req(신규 REQ 없음, OQ-1/2/3). 단위 366 passed(+KIS sandbox 1건은 live .env URL 로 사전 환경 실패·무관), ruff clean. PG 게이트 마이그레이션/드리프트는 라이브 runtime DSN 미사용(파괴적 truncate 회피)·test DSN 자동 검증. 전부 **Implemented** 유지. (change-req)
- 2026-06-09 :: REQ-032 change-req→Implemented (CHG-20260609-01/02, issue #17, BAT-024): `trex-core enter` 에 `--forced-close-at <ISO8601>` (절대시간) 옵션 추가 — 운영자가 매수 진입 명령 하나로 시간 기반 자동청산(forced_close) 시각 지정. CLI 가 buy intent payload 의 기존 `forced_close_at` 필드에 매핑(계약 REQ-007/021·도메인 REQ-066·forced-close 루프 REQ-037 이미 구현·불변, CLI 노출 갭만 보완). 잘못된 형식 제출 전 거부, 미지정 시 미포함(하위호환). 상대시간(`--hold-minutes`) 미채택. AC-5/6 신규. batch-lite(BAT-024) 단위 검증 → **Implemented**. 단위 353 passed, ruff clean. (change-req/confirm-req-implemented)
- 2026-06-08 :: REQ-017/019/029 change-req + REQ-030 Approved→Implemented (dcy-018, BAT-023): ledger write 응답의 권위 cash/quantity 를 portfolio 스냅샷에 write-back(option D 활성화) — 매수/매도 직후 "수량 live·현금 stale" 비대칭 제거. `record_trade`→`LedgerWriteOutcome{result, *_after}`+어댑터 `cash_balance_after`/`asset_quantity_after` 파싱(new+duplicate)(REQ-017), `LedgerWriter` SnapshotStore write-back best-effort+`pending_cash_delta`(REQ-019), 추정 현금 오버레이(`cash_estimated` 라벨)+슬롯 오버레이 유지(REQ-029, OQ-1 B/OQ-4 i), option D 활성화로 REQ-030 Approved→Implemented. 전부 change-req(신규 REQ 없음, OQ-3 a). portfolio-hub#8(`*_after`) 동작 중. 단위 350 passed, 인프라 게이트 385 passed, ruff clean. (change-req/confirm-req-implemented)
- 2026-06-08 :: REQ-028 + REQ-032 change-req→Implemented (CHG-20260608-01/02 / -03/04, dcy-016 후속, BAT-022): `trex-core slots` 가 universe `assets`(portfolio)를 슬롯 상태로 오인 표시하던 갭 정정(10개 슬롯인데 universe 1개·status 없음 — BAT-007 HTTP 슬롯 read 제거 후 슬롯 상태 read 표면 부재가 근인). snapshot envelope 에 read-only per-slot `slots[]`(canonical `slot_to_dict`: slot_id/trading_date/status/symbol/qty/avg_price/entry_price/.../portfolio_id) additive 추가(REQ-028 AC-4, strategy `assets[]` 불변), `trex-core slots` 가 `slots[]`(실제 슬롯 상태) 표시·`--status` 슬롯 status 필터·`universe` 는 `assets[]` 유지(REQ-032). BAT-007 stub/BAT-008 envelope 슬롯 스키마 단일화. 신규 코드 데몬 + 실 인프라 운영 재현 PASS(slots=10·status·DB 일치·universe 분리·비침습). 단위 338 passed, ruff clean. pytest 인프라 게이트는 Human Approval Memo 수용. 둘 다 **Implemented**. (change-req/confirm-req-implemented)