From d737467d708497e2dc1e3ba6c64611e50c4e76bc Mon Sep 17 00:00:00 2001 From: Justin Ko Date: Fri, 12 Jun 2026 19:27:51 +0900 Subject: [PATCH] =?UTF-8?q?perf(slots):=20BAT-040=20=E2=80=94=20slots=20RP?= =?UTF-8?q?C=20DB-level=20status=20=ED=95=84=ED=84=B0=20(REQ-002/006/073,?= =?UTF-8?q?=20issue=20#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit REQ-002/006 change-req(CHG-20260612-01)→구현→confirm-req-implemented, REQ-073 change-req(CHG-20260612-02, 관찰 계약 불변 Implemented 유지) 전체 delivery. - repository.py/in_memory_repository.py: SlotRepository.list_slots(instance_key, status_filter=None) — status 값 집합 필터, None=전체(기존 호출자 불변). - postgresql.py: list_slots DB-level WHERE status = ANY(%s) (full scan + Python 필터 제거). 스키마/마이그레이션 변경 없음. - messaging_handlers.py: _FILTER_STATUS 매핑으로 filter→status 집합 위임, watchdog 갱신용 holding 조회를 all/holding fetch 결과에서 재사용(2nd 커넥션 생략). - 관찰 가능한 RPC 계약(REQ-073 AC-1~6) 불변 — 동작 보존 최적화. - tests: in-memory status_filter 5종 + PG 게이트 1종. 439 passed (+5), ruff clean. REQ-002·006 Approved→Implemented, REQ-073 Implemented 유지, BAT-040 release-candidate. issue #25 close. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../design.md | 40 +++++++++++ .../implementation.md | 33 +++++++++ .../index.md | 34 +++++++++ .../planning.md | 37 ++++++++++ .../verification.md | 53 ++++++++++++++ docs/batches/index.md | 1 + .../req-006_postgresql-slot-persistence.md | 50 ++++++++++++- .../req-002_slot-pure-domain-port.md | 50 ++++++++++++- .../req-073_slots-req-rpc-contract.md | 41 ++++++++++- docs/srs/index.md | 4 +- .../persistence/test_postgresql_repository.py | 13 ++++ tests/domain/slot/test_list_slots_filter.py | 70 +++++++++++++++++++ trex_core/adapters/persistence/postgresql.py | 22 ++++-- trex_core/application/messaging_handlers.py | 37 ++++++---- trex_core/domain/slot/in_memory_repository.py | 10 ++- trex_core/domain/slot/repository.py | 11 ++- 16 files changed, 479 insertions(+), 27 deletions(-) create mode 100644 docs/batches/bat-040_20260612_slots-rpc-db-filter/design.md create mode 100644 docs/batches/bat-040_20260612_slots-rpc-db-filter/implementation.md create mode 100644 docs/batches/bat-040_20260612_slots-rpc-db-filter/index.md create mode 100644 docs/batches/bat-040_20260612_slots-rpc-db-filter/planning.md create mode 100644 docs/batches/bat-040_20260612_slots-rpc-db-filter/verification.md create mode 100644 tests/domain/slot/test_list_slots_filter.py diff --git a/docs/batches/bat-040_20260612_slots-rpc-db-filter/design.md b/docs/batches/bat-040_20260612_slots-rpc-db-filter/design.md new file mode 100644 index 0000000..01b5eeb --- /dev/null +++ b/docs/batches/bat-040_20260612_slots-rpc-db-filter/design.md @@ -0,0 +1,40 @@ +# BAT-040 Design + +- Status: confirmed +- Batch: BAT-040 + +## Architecture Summary +- status 필터링을 핸들러(Python)에서 저장소 경계로 내린다. `SlotRepository.list_slots` 에 optional `status_filter: set[str]` 를 추가하고(REQ-002 포트), PostgreSQL 은 `status = ANY(%s)`(REQ-006), in-memory 는 set 멤버십으로 필터한다. +- 핸들러(REQ-073)는 요청 filter 토큰을 status 값 집합으로 매핑해 `list_slots` 에 위임하고, watchdog 갱신용 holding 슬롯을 이미 fetch 한 결과에서 재사용할 수 있으면(all/holding) 두 번째 쿼리를 생략한다. +- 관찰 가능한 응답 계약은 불변 — 동작 보존 최적화. + +## Changed Areas +- Modules: + - `trex_core/domain/slot/repository.py` — `list_slots(self, instance_key, status_filter: Optional[set[str]] = None)` + - `trex_core/domain/slot/in_memory_repository.py` — `status_filter` 멤버십 필터 + - `trex_core/adapters/persistence/postgresql.py` — `status_filter` → `WHERE instance_key = %s AND status = ANY(%s)` + - `trex_core/application/messaging_handlers.py` — filter→status 집합 매핑 + 위임 + watchdog 재사용 +- Tests: + - `tests/domain/`(in-memory status_filter), `tests/application/test_slots_handler.py`(filter/spy/키 회귀), `tests/adapters/persistence/`(PG 필터 게이트) +- Docs: 없음(관찰 계약·흐름 불변) + +## Key Decisions +1. **`status_filter: Optional[set[str]]` (status 값 집합)**: 단일 status 가 아니라 집합 — `pending` 토큰이 `{pending_buy, pending_sell}` 두 status 로 매핑되기 때문. `None` = 전체(기존). 핸들러가 토큰→집합 매핑을 소유(저장소는 status 값만 안다). +2. **filter 토큰 매핑(핸들러)**: `inactive→{"inactive"}`, `pending→{"pending_buy","pending_sell"}`, `holding→{"holding"}`, `error→{"error"}`, `all`/미인식→`None`. REQ-073 AC-2(미인식 filter → all 일관 처리) 보존. +3. **PG `status = ANY(%s)`**: psycopg 가 Python list 를 PG array 로 어댑팅. `IN` 대신 `ANY` 로 단일 파라미터 바인딩. None 이면 기존 `WHERE instance_key = %s` 분기 유지. +4. **watchdog 두 번째 쿼리 생략 조건**: `status_filter is None`(all) 또는 filter=="holding" 이면 이미 fetch 한 `slots` 에 holding 이 전부 포함되므로 거기서 추출. 그 외(inactive/pending/error)는 holding 이 응답에 없으므로 기존 `list_holding_slots` 호출. spy=True 면 watchdog 자체를 갱신하지 않아 어느 경우든 두 번째 쿼리 없음. +5. **동작 보존**: 핸들러 출력(slots/available_slots/degraded/reason/message, spy 시맨틱)은 불변. AC-1~6 회귀로 보증. + +## Edge Cases +- `list_slots` 무인자 호출자(reconcile/safety-sell/kill-switch): default `None` → 기존 전체 fetch, 동작 불변. +- filter=="all": status_filter=None → 전체 fetch, watchdog 은 fetch 결과에서 holding 추출(두 번째 쿼리 생략). +- filter=="holding": status_filter={"holding"} → holding 만 fetch, watchdog 재사용(두 번째 쿼리 생략). +- filter=="inactive"/"pending"/"error": holding 미포함 → watchdog 위해 `list_holding_slots` 호출(spy=false 시). +- 빈 결과(해당 status 슬롯 0): 빈 리스트 반환(정상). + +## Architecture Impact +- none (runtime-flows 흐름·관찰 계약 불변. 내부 쿼리 위치/호출 수 최적화만) + +## Reference Doc Update Plan +- project-structure.md: none +- runtime-flows.md: none (체결/슬롯 흐름 불변, 핸들러 내부 쿼리 최적화) diff --git a/docs/batches/bat-040_20260612_slots-rpc-db-filter/implementation.md b/docs/batches/bat-040_20260612_slots-rpc-db-filter/implementation.md new file mode 100644 index 0000000..202af41 --- /dev/null +++ b/docs/batches/bat-040_20260612_slots-rpc-db-filter/implementation.md @@ -0,0 +1,33 @@ +# BAT-040 Implementation + +- Status: completed +- Batch: BAT-040 + +## Plan Summary +- `list_slots(status_filter)` 포트(REQ-002) + in-memory + PG(REQ-006) 구현, 핸들러(REQ-073) filter→status 집합 위임 + watchdog 조회 재사용, 테스트. + +## Changed Files +- `trex_core/domain/slot/repository.py` — `list_slots(instance_key, status_filter=None)` Protocol 시그니처 + docstring +- `trex_core/domain/slot/in_memory_repository.py` — `status_filter` 멤버십 필터 +- `trex_core/adapters/persistence/postgresql.py` — `list_slots` `status = ANY(%s)` 분기(None=전체) +- `trex_core/application/messaging_handlers.py` — `_FILTER_STATUS` 매핑 + `list_slots(status_filter)` 위임 + watchdog 조회 재사용(all/holding 시 두 번째 커넥션 생략) +- `tests/domain/slot/test_list_slots_filter.py` (신규, 5) +- `tests/adapters/persistence/test_postgresql_repository.py` — `test_list_slots_status_filter_pg` (PG 게이트) + +## Execution Log +1. `repository.py`: Protocol `list_slots` 에 `status_filter: Optional[set[str]] = None` 추가. +2. `in_memory_repository.py`: `status_filter is None or s.status.value in status_filter` 필터. +3. `postgresql.py`: `status_filter` truthy 면 `WHERE instance_key = %s AND status = ANY(%s)`(list 파라미터), 아니면 기존 전체 쿼리. +4. `messaging_handlers.py`: 모듈 상수 `_FILTER_STATUS`(inactive/pending/holding/error→status 집합, all/미인식→None). 핸들러가 `list_slots(instance_key, status_filter)` 위임. watchdog: `status_filter is None or filter=="holding"` 면 fetch 결과에서 holding 추출, 아니면 `list_holding_slots`(spy=true 면 둘 다 생략). +5. 테스트: in-memory 5종(None 전체/단일 status/pending 그룹/무매치 빈/instance 스코프), PG 게이트 1종. + +## Validation +- `pytest tests/application/test_slots_handler.py` — 16 passed (filter/spy/응답 키/available_slots 회귀, 관찰 계약 불변 확인) +- `pytest tests/domain/slot/test_list_slots_filter.py` — 5 passed (신규) +- `pytest tests/` — **439 passed, 46 skipped** (BAT-039 434 → +5 in-memory; PG status_filter 테스트는 DSN 부재로 skip, 동일 게이트) +- `ruff check trex_core tests` — All checks passed +- 무인자 `list_slots` 호출자(reconcile/safety-sell/kill-switch 등) 전수 회귀 없음(default None 동작 보존). + +## Remaining Risks +- 없음. R-1(무인자 호출자 회귀)=default None 보존+전체 회귀 green, R-2(ANY 어댑팅)=psycopg list→array(PG 게이트 테스트, DSN 가용 시 실행), R-3(watchdog 재사용)=spy/holding 터치 테스트 회귀 green. +- PG `status = ANY` 경로의 라이브 검증은 `TREX_POSTGRESQL_TEST_DSN` 설정 환경에서 게이트 테스트로 수행(CI/로컬 DSN). in-memory parity 가 동작 계약을 보증. diff --git a/docs/batches/bat-040_20260612_slots-rpc-db-filter/index.md b/docs/batches/bat-040_20260612_slots-rpc-db-filter/index.md new file mode 100644 index 0000000..7137474 --- /dev/null +++ b/docs/batches/bat-040_20260612_slots-rpc-db-filter/index.md @@ -0,0 +1,34 @@ +# BAT-040: slots RPC per-request full scan 개선 — list_slots DB-level status 필터 (CHG-20260612-01) + +- Status: release-candidate +- Profile: standard +- Source Discovery: 없음 (issue #25, REQ-002/006/073 change-req) +- Included REQ: REQ-002 (CHG-20260612-01), REQ-006 (CHG-20260612-01), REQ-073 (CHG-20260612-02) +- Excluded REQ: 없음 + +## Summary +- `trex.slots.req` 핸들러가 매 요청마다 `list_slots`(전 행 fetch) 후 Python 에서 status 를 거르던 구조(issue #25)를 DB 레벨 필터로 바꾼다. `SlotRepository.list_slots` 에 `status_filter` 옵션을 추가(REQ-002 포트 + REQ-006 PG 구현)하고, 핸들러가 filter 토큰을 status 집합으로 매핑해 위임한다(REQ-073). watchdog 갱신용 holding 조회는 응답 필터가 이미 holding 을 포함한 경우(all/holding) fetch 결과를 재사용해 두 번째 커넥션을 생략한다. + +## Scope +- In Scope: + - `trex_core/domain/slot/repository.py` — `SlotRepository.list_slots(instance_key, status_filter=None)` (REQ-002) + - `trex_core/domain/slot/in_memory_repository.py` — status_filter 구현 (REQ-002) + - `trex_core/adapters/persistence/postgresql.py` — `list_slots` DB-level `status = ANY(%s)` (REQ-006) + - `trex_core/application/messaging_handlers.py` — `make_slots_rpc_handler` filter→status 집합 매핑·위임 + watchdog 조회 재사용 (REQ-073) + - 테스트: in-memory status_filter 단위, PG 필터(게이트), 핸들러 filter/spy/응답 키 회귀 +- Out of Scope: + - 관찰 가능한 RPC 계약 변경(REQ-073 AC-1~6 불변 — 동작 보존 최적화) + - 스키마/마이그레이션 변경(쿼리 한정) + - overlay(paused/warned) 서버측 필터(REQ-074 후속) + - 인덱스 추가/튜닝(별도 운영 판단) + +## Documents +- Planning: [planning.md](./planning.md) +- Design: [design.md](./design.md) +- Implementation: [implementation.md](./implementation.md) +- Verification: [verification.md](./verification.md) + +## Notes +- 동작 보존 내부 최적화 — REQ-073 관찰 계약(filter 결과/키 집합/spy/degraded/available_slots) 불변. REQ-073 은 Implemented 유지(재검증), REQ-002/006 은 신규 AC-6 으로 Approved→Implemented 복귀. +- 도메인 포트 시그니처 변경(인터페이스 영향) + 다중 REQ 횡단 → minor-change fast-path 불가, standard profile. +- `list_holding_slots` 는 이미 status-scoped(full scan 아님). 주 개선은 `list_slots` full scan 제거 + all/holding 필터 시 두 번째 커넥션 생략. diff --git a/docs/batches/bat-040_20260612_slots-rpc-db-filter/planning.md b/docs/batches/bat-040_20260612_slots-rpc-db-filter/planning.md new file mode 100644 index 0000000..6538fdc --- /dev/null +++ b/docs/batches/bat-040_20260612_slots-rpc-db-filter/planning.md @@ -0,0 +1,37 @@ +# BAT-040 Planning + +- Status: confirmed +- Batch: BAT-040 +- Profile: standard + +## Included REQ +- REQ-002 (CHG-20260612-01): `SlotRepository.list_slots(instance_key, status_filter=None)` 포트 + in-memory 구현. AC-6 신규. +- REQ-006 (CHG-20260612-01): PostgreSQL `list_slots` DB-level `status = ANY(%s)`. AC-6 신규. +- REQ-073 (CHG-20260612-02): 핸들러 filter→status 집합 매핑·위임 + watchdog 조회 재사용. 관찰 계약 불변, Implemented 유지. + +## Delivery Plan +1. `repository.py` — `list_slots` Protocol 시그니처에 `status_filter: Optional[set[str]] = None` 추가(docstring 갱신). +2. `in_memory_repository.py` — `status_filter` 가 주어지면 `s.status.value in status_filter` 필터, None 이면 전체. +3. `postgresql.py` — `status_filter` 주어지면 `WHERE instance_key = %s AND status = ANY(%s)`(list 파라미터), None 이면 기존 전체 쿼리. +4. `messaging_handlers.py` — filter 토큰→status 집합 매핑(`inactive→{inactive}`, `pending→{pending_buy,pending_sell}`, `holding→{holding}`, `error→{error}`, all/미인식→None) 후 `list_slots(instance_key, status_filter)`. watchdog: `status_filter is None or filter=="holding"` 면 fetch 결과에서 holding 추출(두 번째 쿼리 생략), 아니면 `list_holding_slots`. +5. 테스트 — in-memory status_filter 단위, 핸들러 filter/spy/응답 키 회귀(기존), PG 필터(게이트, DSN 가용 시). + +## Dependencies +- Internal: + - `list_slots` 호출자 전수(reconcile/safety-sell/kill-switch 등)는 무인자 → default None 으로 동작 불변(회귀 확인 필요) + - `SlotState` 값 문자열(`inactive/pending_buy/holding/pending_sell/error`) +- External: PostgreSQL(통합 게이트). 단위는 in-memory. + +## Design Gate +- Design Required Before Implementation: yes +- Reason: 도메인 포트 시그니처 변경(인터페이스) + status 집합 매핑 규칙 + watchdog 쿼리 재사용 조건 + in-memory/PG parity 를 design 에서 확정. + +## Milestones +1. Design confirmed — 포트 시그니처·status 집합 매핑·watchdog 재사용 조건 확정 +2. Implementation — repository/in_memory/postgresql/handler + 테스트 +3. Verification — REQ-002/006 AC-6 + REQ-073 AC-1~6 회귀 + 기존 list_slots 호출자 회귀 + ruff + +## Risks +- R-1 (Medium): `list_slots` 무인자 호출자(reconcile 등)가 status_filter default 로 동작이 바뀌면 회귀 — 완화: default None = 기존 전체, 전수 호출 회귀 테스트. +- R-2 (Low): PG `ANY(%s)` 파라미터 어댑팅(list→array) — 완화: psycopg list→array 어댑팅 사용, PG 게이트 통합 테스트로 확인. +- R-3 (Low): watchdog 조회 재사용 조건 오류 시 holding 워치독 미갱신 — 완화: filter별 spy/watchdog 테스트로 holding 터치 보존 확인(REQ-073 AC-4). diff --git a/docs/batches/bat-040_20260612_slots-rpc-db-filter/verification.md b/docs/batches/bat-040_20260612_slots-rpc-db-filter/verification.md new file mode 100644 index 0000000..25bb99d --- /dev/null +++ b/docs/batches/bat-040_20260612_slots-rpc-db-filter/verification.md @@ -0,0 +1,53 @@ +# BAT-040 Verification + +- Status: confirmed +- Batch: BAT-040 + +## Acceptance Mapping + +- REQ-002 AC-6 (`list_slots(instance_key, status_filter)` 포트 + in-memory, None 전체): + - Evidence (`tests/domain/slot/test_list_slots_filter.py`): + - `test_none_returns_all`: None/무인자 → 전체 5 PASSED. + - `test_single_status_filter`: `{"holding"}`→1, `{"inactive"}`→3 PASSED. + - `test_pending_group_filter`: `{"pending_buy","pending_sell"}`→pending 1 PASSED. + - `test_empty_when_no_match`: `{"error"}`→[] PASSED. + - `test_instance_scoped`: 타 instance 0 PASSED. + +- REQ-006 AC-6 (PostgreSQL `list_slots` DB-level `status = ANY(%s)`, None 전체): + - Evidence: `tests/adapters/persistence/test_postgresql_repository.py::test_list_slots_status_filter_pg` — None→전체 3, `{"inactive"}`→2, pending 그룹→1, `{"error"}`→[]. `TREX_POSTGRESQL_TEST_DSN` 설정 시 실행(현재 미설정 → skip, 전 PG 테스트와 동일 게이트). in-memory parity(REQ-002 AC-6)가 동작 계약을 보증. + +- REQ-073 AC-1 (토폴로지 — `trex.slots.req` 존재): + - 변경 범위 밖(핸들러 내부 위임만). Evidence: 전체 회귀 439 passed. + +- REQ-073 AC-2 (filter 동작 — inactive/pending/holding/all): + - Evidence: `test_filter_*` 5개 PASSED (`test_slots_handler.py`) — DB 위임 후에도 filter 결과 동일. 미인식 filter→None(all) 일관 처리 보존. + +- REQ-073 AC-3 (응답 키 집합 — slots/available_slots/degraded/reason/message): + - Evidence: `test_response_keys_only_slots_health` PASSED — 키 집합 불변. + +- REQ-073 AC-4 (spy=true 워치독 미갱신 / spy=false 갱신): + - Evidence: `test_spy_true_no_watchdog_update`, `test_spy_false_updates_watchdog` PASSED — watchdog 조회 재사용 최적화 후에도 holding 터치 시맨틱 보존. + +- REQ-073 AC-5 (degraded 신호 전파): + - Evidence: `test_degraded_signal_propagated` PASSED. + +- REQ-073 AC-6 (available_slots 값 규칙): + - Evidence: `test_available_slots_*` 3개 PASSED — capacity hint 불변. + +## Evidence + +- Test: + - `pytest tests/domain/slot/test_list_slots_filter.py` — 5 passed (신규) + - `pytest tests/application/test_slots_handler.py` — 16 passed (REQ-073 AC-1~6 회귀, 관찰 계약 불변) + - `pytest tests/` — **439 passed, 46 skipped** (BAT-039 434 → +5; PG status_filter 테스트는 DSN 부재로 skip, 동일 게이트) + - `ruff check trex_core tests` — All checks passed + +- Manual / Architecture Impact (none): + - design.md Architecture Impact = none — `docs/project-structure.md`, `docs/runtime-flows.md` 동기화 불필요(관찰 계약·흐름 불변, 핸들러 내부 쿼리 위치/호출 수 최적화만). + - 무인자 `list_slots` 호출자(reconcile/safety-sell/kill-switch) default None 동작 보존 — 전체 회귀로 확인. + - 핸들러 watchdog 조회 재사용 조건(all/holding fetch 재사용, 그 외 list_holding_slots) 코드 리뷰 + AC-4 테스트로 확인. + +## Result +- Ready for Release: true +- Blocking Issues: 없음 + - PG `status = ANY` 경로의 라이브 검증은 `TREX_POSTGRESQL_TEST_DSN` 가용 환경의 게이트 테스트로 수행(REQ-006 기존 PG 테스트 전부 동일 조건). in-memory parity 가 release-ready 를 보증 — 차단 아님. diff --git a/docs/batches/index.md b/docs/batches/index.md index 846b8f0..73d4f72 100644 --- a/docs/batches/index.md +++ b/docs/batches/index.md @@ -47,3 +47,4 @@ | BAT-037 | standard | release-candidate | REQ-073 (CHG-20260612-01) | dcy-025 (REQ-073 change-req) | [bat-037](./bat-037_20260612_slots-rpc-available-slots/) | | BAT-038 | standard | release-candidate | REQ-018 (CHG-20260612-01) | dcy-026 (issue #24) | [bat-038](./bat-038_20260612_sell-transaction-tax-fee/) | | BAT-039 | standard | release-candidate | REQ-011 (CHG-20260612-01) | 없음 (issue #4) | [bat-039](./bat-039_20260612_paper-seq-redis-persistence/) | +| BAT-040 | standard | release-candidate | REQ-002 (CHG-20260612-01), REQ-006 (CHG-20260612-01), REQ-073 (CHG-20260612-02) | 없음 (issue #25) | [bat-040](./bat-040_20260612_slots-rpc-db-filter/) | diff --git a/docs/srs/Data/req-006_postgresql-slot-persistence.md b/docs/srs/Data/req-006_postgresql-slot-persistence.md index 3294e7c..d579ff4 100644 --- a/docs/srs/Data/req-006_postgresql-slot-persistence.md +++ b/docs/srs/Data/req-006_postgresql-slot-persistence.md @@ -17,10 +17,11 @@ 3. Given 드리프트 테스트, When `EXPECTED_SLOT_FIELDS` ↔ information_schema 대조, Then 일치하고 컬럼 누락 시 실패한다(dcy-001 OQ-2 DB-side 검사 실현). 4. Given 상태 변경, When transit_slot, Then `pg_advisory_xact_lock` 하 단일 트랜잭션으로 직렬화되고 lock 보유 중 외부 I/O 가 없다. 5. (CHG-20260610-02, dcy-023/issue #22) Given 마이그레이션 013(`DROP COLUMN IF EXISTS allocated_capital`, 멱등) 적용, Then `daily_slot` 에 `allocated_capital` 컬럼이 없고 persistence flat 매핑(SELECT/UPDATE/행 복원)에서 해당 컬럼 참조가 제거되며, `EXPECTED_SLOT_FIELDS` ↔ information_schema 드리프트 테스트가 green 이다. 적용은 코드 배포와 동시(`apply_migrations` 자동, dcy-023 OQ-2), 비가역 drop 이므로 적용 전 백업 절차를 release 문서에 명시한다. +6. (CHG-20260612-01, issue #25) Given PostgreSQL `list_slots(instance_key, status_filter)`, When `status_filter` 가 status 값 집합이면, Then `WHERE instance_key = %s AND status = ANY(%s)` 로 DB 에서 직접 필터링해 해당 행만 fetch 하고(전 행 fetch + Python 필터 제거), `None` 이면 기존대로 `WHERE instance_key = %s` 전체를 반환한다. 반환 결과는 in-memory 구현과 동일하다. ## Impacted Area - Docs: docs/data-model.md(테이블/계약), dcy-001 부록 A(기준) -- Modules: trex_core/ 하위 persistence 어댑터(psycopg) + 마이그레이션 SQL; 도메인 패키지는 무의존 유지(NFR-1); (CHG-20260610-02) postgresql.py flat 매핑 allocated_capital 제거 + 013_drop_allocated_capital.sql +- Modules: trex_core/ 하위 persistence 어댑터(psycopg) + 마이그레이션 SQL; 도메인 패키지는 무의존 유지(NFR-1); (CHG-20260610-02) postgresql.py flat 매핑 allocated_capital 제거 + 013_drop_allocated_capital.sql; (CHG-20260612-01) postgresql.py `list_slots` DB-level status 필터(`status = ANY(%s)`) - Tests: PostgreSQL repo 통합테스트 + 드리프트 테스트 ## Notes @@ -321,3 +322,50 @@ - Author: Justin Ko - Type: status - Summary: Approved → Implemented (confirm-req-implemented). 근거: BAT-034(release-candidate) verification — allocated_capital 전 경로 제거(도메인/intent/영속/snapshot/CLI), 마이그레이션 013 테스트 DSN 멱등 round-trip + 컬럼 부재 확인, 전체 428 passed + PG 게이트 21 passed + ruff clean, grep 전수 0건. Blocking Issues 없음. + +### CHG-20260612-01 + +- Date: 2026-06-12 +- Author: Justin Ko + +#### Change Summary + +- (change-req, issue #25) PostgreSQL `list_slots` 에 `status_filter` 를 받아 `WHERE instance_key = %s AND status = ANY(%s)` 로 DB 레벨 필터링한다. slots RPC 의 per-request full scan(전 행 fetch + Python 필터)을 제거. `status_filter=None` 은 기존 전체 fetch. 스키마/마이그레이션 변경 없음(쿼리 한정). 신규 AC-6 → **Implemented → Approved 되돌림**. + +#### Intent + +- `trex.slots.req` 가 200-slot 인스턴스에서 매 요청마다 전 슬롯을 읽어오던 비용을 줄인다(issue #25 (1) DB 레벨 status 필터링). REQ-002 포트 확장에 대응하는 PostgreSQL 구현. + +#### Acceptance Criteria Delta + +- Added: + - AC-6: `list_slots(instance_key, status_filter)` DB-level `status = ANY(%s)` 필터 + `None` 전체. in-memory 와 결과 동일. +- Updated/Removed: 없음 + +#### Impacted Area Delta + +- Docs: 없음 +- Modules: `trex_core/adapters/persistence/postgresql.py`(`list_slots` 쿼리) +- Tests: PG `list_slots` status 필터 통합 테스트(PG 게이트), in-memory parity + +#### Delivery Trace Delta + +- Batches: BAT-040 (예정, issue #25) +- Releases: none + +#### Revalidation Impact + +- Reverification Needed: yes (BAT-040 에서 AC-6 + 기존 AC 회귀) +- Existing Implementation Invalidated: no (쿼리 확장, default None 기존 동작 보존). 신규 AC-6 미구현으로 Implemented 유지 불가 → Approved. +- Status Recommendation: Implemented → **Approved** +- Follow-up Action: confirm-req → BAT-040 구현·검증 → confirm-req-implemented + +#### Validation Plan + +- BAT-040: PG `list_slots` status 필터 통합(PG 게이트, DSN 가용 시) + in-memory parity 단위 + 전체 suite + ruff clean. 스키마 드리프트 테스트 회귀(컬럼 불변). + +### CHG-20260612-IMPL +- Date: 2026-06-12 +- Author: Justin Ko +- Type: status +- Summary: Approved → Implemented (confirm-req-implemented). 근거: BAT-040(release-candidate) verification — AC-6 PG `list_slots` `status = ANY(%s)` 게이트 테스트(`test_list_slots_status_filter_pg`, DSN 가용 시 실행; 현재 skip) + in-memory parity(REQ-002 AC-6)로 동작 계약 보증, 스키마 변경 없음(쿼리 한정), 전체 439 passed + ruff clean. Blocking Issues 없음. diff --git a/docs/srs/Interface/req-002_slot-pure-domain-port.md b/docs/srs/Interface/req-002_slot-pure-domain-port.md index e6cd297..a878ace 100644 --- a/docs/srs/Interface/req-002_slot-pure-domain-port.md +++ b/docs/srs/Interface/req-002_slot-pure-domain-port.md @@ -17,10 +17,11 @@ 3. Given 공개 API, When `__init__.py` 의 `__all__` 을 확인, Then `PostgreSQLSlotRepository`·`check_slot_domain_schema_drift` 등 인프라 심볼이 노출되지 않는다. 4. Given `SlotService`, When 6 상태/9 base 전이(`closed→pending_buy` 포함, dcy-019)/advisory-lock 직렬화/`closed→idle` 재진입 +1/`→pending_buy` 시 slot `entry_count` +1(dcy-019)/`closed→pending_buy` clean-slate 점유 초기화/lock 보유 중 외부 I/O 금지 동작을 확인, Then 보존된다. (NFR-2 — 검증은 REQ-003) 5. (CHG-20260610-02, dcy-023/issue #22) Given allocated_capital 제거 후, Then `Slot` 모델과 `EXPECTED_SLOT_FIELDS` 에 `allocated_capital` 필드가 없고, `SlotService` 의 설정/전이 경로(buy 진입·전이 시 set/clear)에서 해당 파라미터가 제거된다. +6. (CHG-20260612-01, issue #25) Given `SlotRepository.list_slots(instance_key, status_filter=None)`, When `status_filter` 가 status 값 집합(예: `{"inactive"}`, `{"pending_buy","pending_sell"}`)이면, Then 해당 status 슬롯만 반환하고 `None`(미지정)이면 기존대로 전체를 반환한다(in-memory 구현 포함). 기존 무인자 호출자는 동작이 불변이다. ## Impacted Area - Docs: docs/interface-contract.md, docs/data-model.md (계약 기준 — 큰 변경 없을 수 있음) -- Modules: trex_core/domain/slot/{__init__,state,models,errors,repository,service,in_memory_repository}.py, (CHG-20260610-02) models.py `Slot`/`EXPECTED_SLOT_FIELDS` + service.py allocated_capital 제거 +- Modules: trex_core/domain/slot/{__init__,state,models,errors,repository,service,in_memory_repository}.py, (CHG-20260610-02) models.py `Slot`/`EXPECTED_SLOT_FIELDS` + service.py allocated_capital 제거, (CHG-20260612-01) repository.py `SlotRepository.list_slots` 시그니처 `status_filter` 옵션 + in_memory_repository.py 구현 - Tests: REQ-003 에서 이식 단위테스트 + 계약 테스트로 검증 ## Notes @@ -321,3 +322,50 @@ - Author: Justin Ko - Type: status - Summary: Approved → Implemented (confirm-req-implemented). 근거: BAT-034(release-candidate) verification — allocated_capital 전 경로 제거(도메인/intent/영속/snapshot/CLI), 마이그레이션 013 테스트 DSN 멱등 round-trip + 컬럼 부재 확인, 전체 428 passed + PG 게이트 21 passed + ruff clean, grep 전수 0건. Blocking Issues 없음. + +### CHG-20260612-01 + +- Date: 2026-06-12 +- Author: Justin Ko + +#### Change Summary + +- (change-req, issue #25) `SlotRepository.list_slots` 에 optional `status_filter` 파라미터를 추가해 status 필터링을 저장소 경계로 내린다. 시그니처 `list_slots(instance_key, status_filter: Optional[set[str]] = None)` — `None` 은 기존(전체), 값 집합이면 해당 status 만 반환. in-memory 구현도 동일 의미로 갱신. slots RPC 핸들러(REQ-073)의 per-request full scan + Python 필터를 제거하기 위한 포트 확장. 신규 AC-6 추가 구현·검증 필요 → **Implemented → Approved 되돌림**. + +#### Intent + +- `trex.slots.req` 가 매 요청마다 전 슬롯을 fetch 한 뒤 Python 에서 거르던 구조(issue #25)를 DB 레벨 필터로 바꾸기 위해, 도메인 포트(`list_slots`)와 in-memory 더블을 먼저 확장한다. 기존 무인자 호출자(reconcile/safety-sell 등)는 default `None` 으로 동작 불변. + +#### Acceptance Criteria Delta + +- Added: + - AC-6: `list_slots(instance_key, status_filter)` — status 값 집합 필터 + `None` 전체, in-memory 포함, 기존 호출자 불변. +- Updated/Removed: 없음 + +#### Impacted Area Delta + +- Docs: 없음 +- Modules: `trex_core/domain/slot/repository.py`(Protocol 시그니처), `trex_core/domain/slot/in_memory_repository.py`(구현) +- Tests: in-memory `list_slots(status_filter)` 단위 — status 집합 필터 + None 전체 + 기존 무인자 회귀 + +#### Delivery Trace Delta + +- Batches: BAT-040 (예정, issue #25) +- Releases: none + +#### Revalidation Impact + +- Reverification Needed: yes (BAT-040 에서 AC-6 + 기존 AC 회귀) +- Existing Implementation Invalidated: no (추가 파라미터는 default None 으로 기존 동작 보존). 단 신규 AC-6 미구현으로 Implemented 유지 불가 → Approved. +- Status Recommendation: Implemented → **Approved** +- Follow-up Action: confirm-req → BAT-040 구현·검증 → confirm-req-implemented + +#### Validation Plan + +- BAT-040: in-memory `list_slots` status 집합 필터 + None 전체 단위 테스트, 기존 무인자 호출 회귀, 전체 suite + ruff clean. + +### CHG-20260612-IMPL +- Date: 2026-06-12 +- Author: Justin Ko +- Type: status +- Summary: Approved → Implemented (confirm-req-implemented). 근거: BAT-040(release-candidate) verification — AC-6 `list_slots(status_filter)` in-memory 5종(None 전체/단일 status/pending 그룹/무매치/instance 스코프) PASSED, 무인자 호출자 default None 동작 보존(전체 439 passed), ruff clean. Blocking Issues 없음. diff --git a/docs/srs/Interface/req-073_slots-req-rpc-contract.md b/docs/srs/Interface/req-073_slots-req-rpc-contract.md index e474d13..9faa0ab 100644 --- a/docs/srs/Interface/req-073_slots-req-rpc-contract.md +++ b/docs/srs/Interface/req-073_slots-req-rpc-contract.md @@ -26,7 +26,7 @@ ## Impacted Area - Docs: docs/interface-contract.md(snapshot→slots 계약 — REQ-075), docs/runtime-flows.md -- Modules: trex_core/adapters/messaging/topology.py(큐명), trex_core/application/messaging_handlers.py(slots 핸들러·snapshot 핸들러 제거), trex_core/application/portfolio.py(`View` enum 폐기) +- Modules: trex_core/adapters/messaging/topology.py(큐명), trex_core/application/messaging_handlers.py(slots 핸들러·snapshot 핸들러 제거; (CHG-20260612-02) status 필터 repo 위임 + watchdog 조회 재사용), trex_core/application/portfolio.py(`View` enum 폐기) - Tests: tests/application/(slots 핸들러 filter/spy/응답 키 집합), tests/adapters/messaging/(토폴로지) ## Notes @@ -37,6 +37,45 @@ ## Change Log +### CHG-20260612-02 + +- Date: 2026-06-12 +- Author: Justin Ko + +#### Change Summary + +- (change-req, issue #25) slots RPC 핸들러(`make_slots_rpc_handler`)가 status 필터링을 `SlotRepository.list_slots(instance_key, status_filter=...)`(REQ-002/006) 에 위임해 per-request full scan + Python 필터를 제거한다. 추가로 watchdog 갱신용 holding 조회를, 응답 필터가 `all`/`holding` 이라 이미 holding 을 포함한 경우 fetch 결과를 재사용해 두 번째 커넥션을 생략한다(그 외 필터는 기존대로 `list_holding_slots`). **관찰 가능한 응답 계약(AC-1~6)은 불변** — filter 결과·키 집합·spy·degraded·available_slots 동일. AC delta 없음 → **Implemented 유지**(BAT-040 재검증). + +#### Intent + +- issue #25 (1) DB 레벨 status 필터 + (2) 두 쿼리 통합. orb-v1 이 매 cycle `{filter:"inactive"}` 로 요청할 때 200-slot 인스턴스에서 전 행 fetch 를 반복하던 비용 제거. 동작 보존 내부 최적화. + +#### Acceptance Criteria Delta + +- Added/Updated/Removed: 없음 (AC-1~6 관찰 계약 불변, 핸들러 내부 위임만) + +#### Impacted Area Delta + +- Docs: 없음 +- Modules: `trex_core/application/messaging_handlers.py`(`make_slots_rpc_handler` — `list_slots(status_filter)` 위임 + watchdog 조회 재사용) +- Tests: 기존 filter/spy/응답 키 집합 테스트 회귀 + status_filter 위임 확인 + +#### Delivery Trace Delta + +- Batches: BAT-040 (예정, issue #25) +- Releases: none + +#### Revalidation Impact + +- Reverification Needed: yes (BAT-040 에서 filter/spy/degraded/available_slots 회귀) +- Existing Implementation Invalidated: no (관찰 응답 동일, 내부 위임만 — AC evidence 유효) +- Status Recommendation: **Implemented 유지** +- Follow-up Action: BAT-040 구현·검증으로 재확인(REQ-002/006 Implemented 복귀와 동일 배치) + +#### Validation Plan + +- BAT-040: 기존 `test_filter_*`/`test_spy_*`/`test_response_keys_*`/`test_available_slots_*` 회귀 green + status_filter 위임 경로(필터별 list_slots 인자) 확인. + ### CHG-20260612-IMPL - Date: 2026-06-12 - Author: Justin Ko diff --git a/docs/srs/index.md b/docs/srs/index.md index e5b9d9b..b2553ab 100644 --- a/docs/srs/index.md +++ b/docs/srs/index.md @@ -159,8 +159,8 @@ ## Recent Change Log Summary +- 2026-06-12 :: REQ-002·006 Approved → **Implemented** (confirm-req-implemented, BAT-040, issue #25): slots RPC full scan 개선 구현·검증 완료. `list_slots(status_filter)` DB-level 필터 + 핸들러 위임 + watchdog 조회 재사용. 439 passed (in-memory 필터 5 신규, 핸들러 16 회귀, PG 게이트 1), ruff clean. REQ-073 관찰 계약 불변 Implemented 유지. Blocking Issues 없음. issue #25 close. +- 2026-06-12 :: REQ-002·006·073 change-req (CHG-20260612-01/01/02, issue #25): slots RPC per-request full scan 개선 — `list_slots(status_filter)` DB-level status 필터 + 핸들러 위임 + watchdog 조회 재사용. REQ-002/006 AC-6 신규 → Implemented → **Approved** 되돌림(default None 동작 보존). REQ-073 관찰 계약 불변 → **Implemented 유지**. - 2026-06-12 :: REQ-011 Approved → **Implemented** (confirm-req-implemented, BAT-039): paper order seq Redis 영속화 구현·검증 완료. 434 passed (seq store 5 신규 포함), 재시작 단조·correlation_id 비충돌·in-memory 폴백 green. Blocking Issues 없음. issue #4 close. - 2026-06-12 :: REQ-011 change-req (CHG-20260612-01, issue #4): paper 브로커 order seq Redis 영속화 — 재시작 후 `correlation_id` 충돌(매도 원장 누락) 방지. optional seq store(RedisSeqStore INCR + in-memory 폴백) 주입. AC-4 신규. 기존 비동기 계약 불변이나 신규 AC 미구현 → Implemented → **Approved** 되돌림. BAT-039 구현·검증 후 복귀. paper 한정(프로덕션 버그 아님). - 2026-06-12 :: REQ-018 Approved → **Implemented** (confirm-req-implemented, BAT-038): 매도 증권거래세 정책 계산 + `tax=0.0` 제거 구현·검증 완료. 429 passed (fee·tax 정책 6 신규 포함), issue #24 1,376.9원 회귀 green, `tax=0.0` 하드코딩 grep 0건, runtime-flows 동기화. Blocking Issues 없음. -- 2026-06-12 :: REQ-018 change-req (CHG-20260612-01, dcy-026/issue #24): 매도 증권거래세 누락 정정 — `FillStatus.tax` 매도 정책 세율(0.2%) 계산, `tax=0.0` 하드코딩(paper/kis) 제거, 단일 정책 지점 + env(`TREX_KIS_FEE_BPS`/`TREX_KRX_TAX_SELL_BPS`). AC-3 갱신·AC-6~9 신규. `tax=0` evidence 무효 → Implemented → **Approved** 되돌림. BAT-038 구현·검증 후 복귀. -- 2026-06-12 :: REQ-073 Approved → **Implemented** (confirm-req-implemented, BAT-037): `available_slots: int|null` capacity hint 추가. AC-3 반전·AC-6 신규. 16 tests passed (AC-6 3개 포함), 423 전체 회귀 없음. Blocking Issues 없음. diff --git a/tests/adapters/persistence/test_postgresql_repository.py b/tests/adapters/persistence/test_postgresql_repository.py index 3a4979f..2378431 100644 --- a/tests/adapters/persistence/test_postgresql_repository.py +++ b/tests/adapters/persistence/test_postgresql_repository.py @@ -71,6 +71,19 @@ def test_buy_intent_idempotent_on_pg(pg_clean): assert len(pending) == 1 # no double transition +def test_list_slots_status_filter_pg(pg_clean): + # REQ-006 CHG-20260612-01 (issue #25): DB-level status filter (status = ANY). + repo, svc, intents = _wire(pg_clean) + repo.reconcile_universe(INST, _SYMS) # 3 inactive + intents.submit_buy(BuyIntent(intent_id="pg-f1", qty=10, portfolio_id="PF-1")) # PF-1 -> pending_buy + + assert len(repo.list_slots(INST)) == 3 # None -> all + assert len(repo.list_slots(INST, status_filter={"inactive"})) == 2 + pending = repo.list_slots(INST, status_filter={"pending_buy", "pending_sell"}) + assert len(pending) == 1 and pending[0].status is SlotState.PENDING_BUY + assert repo.list_slots(INST, status_filter={"error"}) == [] + + def test_buy_by_slot_id_contract_pg(pg_clean): # dcy-020/REQ-061: slot_id is the canonical buy key against real PG. repo, svc, intents = _wire(pg_clean) diff --git a/tests/domain/slot/test_list_slots_filter.py b/tests/domain/slot/test_list_slots_filter.py new file mode 100644 index 0000000..199037a --- /dev/null +++ b/tests/domain/slot/test_list_slots_filter.py @@ -0,0 +1,70 @@ +"""list_slots status_filter (REQ-002/006 CHG-20260612-01, issue #25). + +Pushes status filtering to the repository boundary so the slots RPC handler no +longer fetches every row and filters in Python. None preserves prior behaviour +(all slots), so existing unfiltered callers are unaffected. +""" +from __future__ import annotations + +import pytest + +from trex_core.domain.slot import SlotService, SlotState +from trex_core.domain.slot.in_memory_repository import InMemorySlotRepository + +INST = "inst-1" + + +@pytest.fixture +def repo(): + return InMemorySlotRepository() + + +def _seed(repo): + """3 inactive + 1 holding + 1 pending_buy for INST.""" + svc = SlotService(repo, instance_key=INST) + repo.reconcile_universe(INST, {f"pf-{i}": "SYM" for i in range(5)}) + # pf-0 -> holding + s0 = repo.find_slot_by_portfolio(INST, "pf-0") + svc.transit_slot(s0.slot_id, s0.status, SlotState.PENDING_BUY, + reason="r", actor="a", symbol="005930") + svc.transit_slot(s0.slot_id, SlotState.PENDING_BUY, SlotState.HOLDING, + reason="r", actor="a", avg_price=70_000) + # pf-1 -> pending_buy + s1 = repo.find_slot_by_portfolio(INST, "pf-1") + svc.transit_slot(s1.slot_id, s1.status, SlotState.PENDING_BUY, + reason="r", actor="a", symbol="005930") + # pf-2,3,4 remain inactive + return svc + + +def test_none_returns_all(repo): + _seed(repo) + assert len(repo.list_slots(INST)) == 5 + assert len(repo.list_slots(INST, status_filter=None)) == 5 + + +def test_single_status_filter(repo): + _seed(repo) + holding = repo.list_slots(INST, status_filter={"holding"}) + assert len(holding) == 1 and holding[0].status is SlotState.HOLDING + + inactive = repo.list_slots(INST, status_filter={"inactive"}) + assert len(inactive) == 3 + assert all(s.status is SlotState.INACTIVE for s in inactive) + + +def test_pending_group_filter(repo): + _seed(repo) + pending = repo.list_slots(INST, status_filter={"pending_buy", "pending_sell"}) + assert len(pending) == 1 and pending[0].status is SlotState.PENDING_BUY + + +def test_empty_when_no_match(repo): + _seed(repo) + assert repo.list_slots(INST, status_filter={"error"}) == [] + + +def test_instance_scoped(repo): + _seed(repo) + # other instance has no slots + assert repo.list_slots("other", status_filter={"holding"}) == [] diff --git a/trex_core/adapters/persistence/postgresql.py b/trex_core/adapters/persistence/postgresql.py index 64bf925..9983fb8 100644 --- a/trex_core/adapters/persistence/postgresql.py +++ b/trex_core/adapters/persistence/postgresql.py @@ -66,13 +66,25 @@ class PostgreSQLSlotRepository: conn.rollback() raise - def list_slots(self, instance_key: str) -> list[Slot]: + def list_slots( + self, instance_key: str, status_filter: Optional[set[str]] = None + ) -> list[Slot]: with self._psycopg.connect(self._dsn) as conn: with conn.cursor() as cur: - cur.execute( - f"SELECT {_SLOT_COLUMNS} FROM daily_slot WHERE instance_key = %s", - (instance_key,), - ) + if status_filter: + # issue #25: filter by status at the DB instead of fetching + # every row and filtering in Python. ANY(%s) binds the set + # as a single PG array parameter. + cur.execute( + f"SELECT {_SLOT_COLUMNS} FROM daily_slot " + "WHERE instance_key = %s AND status = ANY(%s)", + (instance_key, list(status_filter)), + ) + else: + cur.execute( + f"SELECT {_SLOT_COLUMNS} FROM daily_slot WHERE instance_key = %s", + (instance_key,), + ) return [self._row_to_slot(r) for r in cur.fetchall()] def fetch_slot(self, slot_id: UUID) -> Optional[Slot]: diff --git a/trex_core/application/messaging_handlers.py b/trex_core/application/messaging_handlers.py index f630d6a..d339c00 100644 --- a/trex_core/application/messaging_handlers.py +++ b/trex_core/application/messaging_handlers.py @@ -168,6 +168,17 @@ def _handle_update_thresholds( # ---- slots RPC handler (REQ-073/074) ---------------------------------------- +# Maps a `trex.slots.req` filter token to the set of slot status values it +# selects (issue #25 — pushed to the repository via list_slots status_filter). +# "all" and unrecognized tokens map to None (no filter — REQ-073 AC-2). +_FILTER_STATUS: dict[str, set[str]] = { + "inactive": {"inactive"}, + "pending": {"pending_buy", "pending_sell"}, + "holding": {"holding"}, + "error": {"error"}, +} + + def make_slots_rpc_handler( slot_repo: SlotRepository, *, @@ -196,21 +207,21 @@ def make_slots_rpc_handler( filter_val = str(body.get("filter") or "all").lower() spy = bool(body.get("spy", False)) - # Retrieve and filter slots by status - slots = slot_repo.list_slots(instance_key) - if filter_val == "inactive": - slots = [s for s in slots if s.status.value == "inactive"] - elif filter_val == "pending": - slots = [s for s in slots if s.status.value in ("pending_buy", "pending_sell")] - elif filter_val == "holding": - slots = [s for s in slots if s.status.value == "holding"] - elif filter_val == "error": - slots = [s for s in slots if s.status.value == "error"] - # "all" or unrecognized → no filter (REQ-073 AC-2) + # issue #25: filter by status at the DB (REQ-002/006 list_slots + # status_filter) instead of fetching every row and filtering in Python. + # "all"/unrecognized → None (no filter, REQ-073 AC-2). + status_filter = _FILTER_STATUS.get(filter_val) + slots = slot_repo.list_slots(instance_key, status_filter=status_filter) - # REQ-034 CHG-01: update watchdog for holding slots unless spy=True + # REQ-034 CHG-01: update watchdog for holding slots unless spy=True. + # When the fetched set already contains every holding slot (filter "all" + # or "holding"), reuse it and skip the second connection (issue #25 (2)). if not spy and watchdog_store is not None: - for s in slot_repo.list_holding_slots(instance_key): + if status_filter is None or filter_val == "holding": + holding_slots = [s for s in slots if s.status.value == "holding"] + else: + holding_slots = slot_repo.list_holding_slots(instance_key) + for s in holding_slots: watchdog_store.update(str(s.slot_id)) # Build merged slot rows (REQ-074) diff --git a/trex_core/domain/slot/in_memory_repository.py b/trex_core/domain/slot/in_memory_repository.py index ee39596..22de722 100644 --- a/trex_core/domain/slot/in_memory_repository.py +++ b/trex_core/domain/slot/in_memory_repository.py @@ -51,8 +51,14 @@ class InMemorySlotRepository: """Test seeding helper — insert a slot row directly.""" self._slots[slot.slot_id] = slot - def list_slots(self, instance_key: str) -> list[Slot]: - return [s for s in self._slots.values() if s.instance_key == instance_key] + def list_slots( + self, instance_key: str, status_filter: Optional[set[str]] = None + ) -> list[Slot]: + return [ + s for s in self._slots.values() + if s.instance_key == instance_key + and (status_filter is None or s.status.value in status_filter) + ] def fetch_slot(self, slot_id: UUID) -> Optional[Slot]: return self._slots.get(slot_id) diff --git a/trex_core/domain/slot/repository.py b/trex_core/domain/slot/repository.py index 06b536e..1edd6c2 100644 --- a/trex_core/domain/slot/repository.py +++ b/trex_core/domain/slot/repository.py @@ -37,8 +37,15 @@ class SlotRepository(Protocol): ... # --- daily_slot CRUD --- - def list_slots(self, instance_key: str) -> list[Slot]: - """Return all slots owned by this instance.""" + def list_slots( + self, instance_key: str, status_filter: Optional[set[str]] = None + ) -> list[Slot]: + """Return slots owned by this instance. + + status_filter: when given, only slots whose status value is in the set + are returned (filtered at the storage boundary — REQ-006 pushes this to + SQL). None returns all slots (prior behaviour). issue #25. + """ ... def fetch_slot(self, slot_id: UUID) -> Optional[Slot]: