feat(alarm): trex-core 알람 emitter + trex.alerts exchange (dcy-033 BAT-049, Implemented)

AlarmEmitter(application, best-effort 비차단)+trex.alerts topic exchange 가산·격리+8 CRITICAL code 배선. 608 passed, opus QC PASS, 팀장 e2e(텔레그램 HTTP200) 통과. REQ-098~101 Implemented.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
This commit is contained in:
2026-06-21 19:25:55 +09:00
co-authored by Claude Opus 4.8
parent b2165eac25
commit d4c5fcbba1
29 changed files with 1195 additions and 47 deletions
@@ -0,0 +1,120 @@
# BAT-049 Design
- Status: confirmed
- Batch: BAT-049
## Architecture Summary
알람 시스템을 application 레이어의 `AlarmEmitter`(포트 경유) + adapters 레이어의 `publish_alarm_event()` 확장으로 구현한다. 기존 레이어드 아키텍처(domain/application/adapters/runtime)와 의존성 역전 규칙을 그대로 따른다.
```
발생 지점(execution.py 등)
│ alarm_emitter.publish_alarm(code, title, fields)
▼
AlarmEmitter (application 레이어)
│ MessagingTransport.publish_alarm_event(routing_key, payload)
▼
RabbitMQTransport.publish_alarm_event() (adapters 레이어)
│ basic_publish(exchange=trex.alerts, routing_key=alarm.critical.trex-core, mandatory=False)
▼
trex.alerts exchange → (notifier가 alarm.# 패턴으로 바인딩)
```
`AlarmEmitter`는 `MessagingTransport` 포트만 주입받는다. pika 직접 import 없음. domain 레이어 접촉 없음.
## Changed Areas
| 파일 | 변경 유형 | 내용 |
|---|---|---|
| `docs/interface-contract.md` | 추가(기존 불변) | `Interface: trex.alerts (알람 채널, dcy-033)` 섹션 신규 |
| `trex_core/adapters/messaging/topology.py` | 추가 | `ALERTS_EXCHANGE = "trex.alerts"` 상수 추가 |
| `trex_core/adapters/messaging/rabbitmq.py` | 확장 | `declare_topology()`에 trex.alerts exchange 선언 + `publish_alarm_event()` 메서드 신규 |
| `trex_core/adapters/messaging/in_memory.py` | 확장 | `publish_alarm_event()` stub 추가 (발행 기록 + unavailable 시 TransportUnavailableError) |
| `trex_core/application/messaging.py` | 확장 | `MessagingTransport` Protocol에 `publish_alarm_event()` 추가 |
| `trex_core/application/alarm_emitter.py` | 신규 | `AlarmEmitter` 클래스 구현 |
| `trex_core/application/execution.py` | 배선 | `fill_price_unresolvable`, `sell_skipped_no_balance` 알람 호출 추가 |
| `trex_core/application/reconciliation.py` | 배선 | `core_holding_broker_flat` 알람 호출 추가 |
| `trex_core/application/ledger_outbox.py` | 배선 | `ledger_delivery_failed` 알람 호출 추가 |
| `trex_core/application/killswitch.py` | 배선 | `kill_switch_fired` 알람 호출 추가 |
| `trex_core/adapters/broker/rabbitmq.py`(`messaging/rabbitmq.py`) | 배선 | `publish_unavailable_after_reconnect` / `dlq_publish_failed` — 콜백 패턴 |
| `trex_core/application/portfolio.py` / `snapshot_scheduler.py` | 배선 | `portfolio_hub_unavailable` 알람 호출 추가 |
| `trex_core/runtime/reset.py` | 배선 | `schema_verification_failed` / `migration_failed` 알람 호출 (transport None 안전) |
| `trex_core/runtime/daemon.py` | 조립 | `AlarmEmitter` 생성 + 각 서비스에 주입 |
| `tests/application/test_alarm_emitter.py` | 신규 | AlarmEmitter 단위 테스트 |
| `tests/adapters/messaging/test_topology.py` | 확장 | declare_topology trex.alerts assertion |
| `docs/runtime-flows.md` | 추가(기존 불변) | 알람 발신 흐름 섹션 신규 |
## Key Decisions
### KD-1: publish_unavailable_after_reconnect / dlq_publish_failed — 콜백 패턴 채택
**문제**: 두 alarm code는 `trex_core/adapters/messaging/rabbitmq.py`(어댑터) 내부에서 발생하지만, `AlarmEmitter`는 application 레이어에 있다. 어댑터가 application을 직접 import하면 의존성이 역전된다.
**채택 결정: 콜백(callable) 주입**
- `RabbitMQTransport`에 `alarm_callback: Optional[Callable[[str, str, dict], None]]` 파라미터를 추가한다. 시그니처: `(code: str, title: str, fields: dict) -> None`.
- `daemon.py`에서 `AlarmEmitter.publish_alarm`을 콜백으로 주입한다.
- 어댑터는 application 타입을 import하지 않고, callable 타입 힌트(`Optional[Callable]`)만 사용.
- 콜백 미주입 시(기본값 None): 기존 동작 유지 (로그만).
- 콜백이 raise하면 어댑터는 except로 삼킨다(best-effort 원칙 일관).
**기각 대안**:
- 이벤트 버스/큐: 과도한 복잡성, 결정론적 단위 테스트 어려움.
- AlarmEmitter를 adapters 레이어로 이동: domain/application 순수성 원칙 위반.
- Transport 프로토콜에 alarm_callback getter 추가: Protocol 오염 + 테스트 어려움.
### KD-2: reset.py transport None 안전 — AlarmEmitter None-guard 패턴
**문제**: `reset.py`는 CLI 진입점으로 `MessagingTransport`가 없다. AlarmEmitter에 None transport를 전달하면 호출 시 crash.
**채택 결정**: `AlarmEmitter.__init__`이 `Optional[MessagingTransport]`를 받고, `publish_alarm()` 내부에서 transport가 None이면 WARN 로그 후 skip.
- `reset.py`는 `AlarmEmitter(transport=None)`로 생성하거나 `publish_alarm()` 호출 전 None 체크.
- 실제로는 `run_reset()` 함수가 `AlarmEmitter`를 직접 받는 시그니처로 확장하거나, reset 전용 경량 emitter를 분리한다.
- 가장 단순한 방법: `AlarmEmitter(transport=None)` 허용 + None guard → reset 코드에서 생성 부담 최소화.
### KD-3: publish_alarm_event 시그니처 — transport 레이어 알람 전용 메서드
**결정**: `MessagingTransport` 프로토콜에 `publish_alarm_event(routing_key: str, payload: dict) -> None` 추가.
- `publish_event()`와 구분하는 이유: alarm은 `trex.alerts` exchange, `mandatory=False`, `delivery_mode=1`(best-effort, 비영속) vs events는 `trex.events`, `mandatory=True`, `delivery_mode=2`.
- `InMemoryTransport`: 발행된 alarm을 `_alarm_events: list[tuple[str, dict]]`에 기록 + unavailable 시 TransportUnavailableError 발생(호출자가 삼킴).
- `RabbitMQTransport`: `publish_alarm_event()`는 `basic_publish(exchange=trex.alerts, mandatory=False, delivery_mode=1)`. 연결 실패 시 TransportUnavailableError raise(AlarmEmitter가 삼킴). retry-on-failure 없음(best-effort 원칙).
### KD-4: AlarmEmitter 인터페이스
```python
class AlarmEmitter:
def __init__(self, transport: Optional[MessagingTransport]) -> None: ...
def publish_alarm(
self,
code: str,
title: str,
*,
fields: dict | None = None,
severity: str = "critical",
) -> None:
"""Best-effort 발행. 실패 시 ERROR 로그 후 예외 삼킴."""
...
```
- `source="trex-core"`, `schema_version="1"` 상수화.
- `occurred_at` = 발행 시점 KST ISO-8601.
- routing key = `alarm.{severity}.trex-core`.
- 발행 실패(TransportUnavailableError, 기타 모든 예외): ERROR 로그 후 예외 삼킴(re-raise 금지).
## Edge Cases
- **transport disconnect 중 알람 발행**: `publish_alarm_event()` → TransportUnavailableError → AlarmEmitter가 ERROR 로그 후 삼킴. 본업 경로 비차단.
- **rabbitmq.py 콜백이 None인 경우**: `alarm_callback` guard로 skip. 기존 코드 변경 없이 하위호환.
- **reset.py에서 AlarmEmitter(transport=None)**: None guard 분기 → INFO 로그("alarm skipped: no transport") 후 skip.
- **mandatory=False + notifier 미연결**: silent drop. UnroutableError 없음.
- **8개 code 중 일부 경로에서 AlarmEmitter 미주입**: daemon.py에서 전부 주입. 미주입 시 AttributeError → execution_loop except로 삼킴(기존 보호 패턴).
- **AlarmEmitter.publish_alarm 내 예외 루프**: 각 호출마다 독립 try/except. 한 코드 발행 실패가 동일 tick의 다른 호출에 영향 없음.
## Architecture Impact
`both` — runtime-flows.md + interface-contract.md 양쪽 업데이트 필요.
## Reference Doc Update Plan
- `docs/interface-contract.md`: `Interface: trex.alerts (알람 채널, dcy-033)` 섹션 추가 + Change Log 갱신 + Last Updated 갱신.
- `docs/runtime-flows.md`: `Flow: 알람 발신 (trex.alerts, dcy-033)` 섹션 추가. 기존 섹션 불변.
@@ -0,0 +1,92 @@
# BAT-049 Implementation
- Status: completed
- Batch: BAT-049
## Plan Summary
REQ-098(문서) → REQ-099(topology+transport) → REQ-100(AlarmEmitter+배선) → REQ-101(runtime-flows 정합) 순서로 구현.
design.md의 4개 Key Decision을 그대로 따름.
## Changed Files
| 파일 | 변경 유형 | 내용 |
|---|---|---|
| `docs/interface-contract.md` | 섹션 추가 + 헤더 갱신 | `Interface: trex.alerts (알람 채널, dcy-033)` 신규 섹션 (7필드 스키마, routing key, topology, CRITICAL code 목록, Update Trigger). Change Log + Last Updated 갱신. 기존 섹션 불변. |
| `trex_core/adapters/messaging/topology.py` | 추가 | `ALERTS_EXCHANGE = "trex.alerts"` + `alarm_routing_key()` 함수 |
| `trex_core/adapters/messaging/rabbitmq.py` | 확장 | `declare_topology()` trex.alerts 선언 추가. `publish_alarm_event()` 신규. `set_alarm_callback()` + `_fire_alarm_callback()` 신규. 콜백 호출 배선 (publish_unavailable_after_reconnect, dlq_publish_failed). `Callable` import 추가. |
| `trex_core/adapters/messaging/in_memory.py` | 확장 | `_alarm_events` 리스트 추가. `publish_alarm_event()` stub + `alarm_events()` 조회. |
| `trex_core/application/messaging.py` | 확장 | `MessagingTransport` Protocol에 `publish_alarm_event()` 추가. |
| `trex_core/application/alarm_emitter.py` | 신규 | `AlarmEmitter` 클래스. envelope 구성, KST occurred_at, 예외 삼킴+ERROR 로그, None transport skip. |
| `trex_core/application/execution.py` | 배선 | `AlarmEmitter` import + `alarm_emitter` 파라미터 + `sell_skipped_no_balance` + `fill_price_unresolvable` 발행 배선. |
| `trex_core/application/reconciliation.py` | 배선 | `AlarmEmitter` import + `alarm_emitter` 파라미터 + `core_holding_broker_flat` 발행 배선. |
| `trex_core/application/ledger_outbox.py` | 배선 | `AlarmEmitter` import + `alarm_emitter` 파라미터 + `ledger_delivery_failed` 3지점(conflict_blocked, permanent_failure, dead_lettered) 배선. |
| `trex_core/application/killswitch.py` | 배선 | `AlarmEmitter` import + `alarm_emitter` 파라미터 + `kill_switch_fired` 배선. |
| `trex_core/application/snapshot_scheduler.py` | 배선 | `AlarmEmitter` import + `alarm_emitter` 파라미터 + `portfolio_hub_unavailable` 2지점(whoami_failed, get_portfolios_failed) 배선. `reconcile_core_holdings` 호출에 `alarm_emitter` 전달. |
| `trex_core/runtime/reset.py` | 배선 | `_emit_reset_alarm()` 헬퍼 추가. `run_reset()` alarm_emitter 파라미터 추가. migration 예외 + schema_ok=False 지점 배선. |
| `trex_core/runtime/daemon.py` | 조립 | `AlarmEmitter` import. `alarm_emitter` 생성 (event_publisher 이후). `set_alarm_callback()` 주입. `LedgerWriter`, `KillSwitchService`, `SnapshotScheduler`, `ExecutionService` 생성 시 alarm_emitter 주입. 서비스 생성 순서 재배치(alarm_emitter 생성 후 주입 보장). |
| `tests/application/test_alarm_emitter.py` | 신규 | AlarmEmitter 단위 테스트 29개 (8 code × envelope 검증, 8 code × TransportUnavailableError 삼킴, 8 code × 임의 예외 삼킴, None transport skip, fields default, routing key, pika 미import, TELEGRAM 미참조). |
| `tests/adapters/messaging/test_topology.py` | 확장 | `test_alerts_exchange_constant`, `test_alarm_routing_key_format` 추가. |
| `tests/adapters/messaging/test_in_memory_transport.py` | 확장 | `test_publish_alarm_event_records_event`, `test_publish_alarm_event_raises_when_unavailable` 추가. |
| `docs/runtime-flows.md` | 섹션 추가 + 헤더 갱신 | `Flow: 알람 발신 (trex.alerts, dcy-033)` 신규 섹션 (발행 흐름, best-effort 정책, 8종 code 목록, trex.events 차이표). 기존 섹션 불변. |
## Design Decisions Implemented
**KD-1 (rabbitmq.py 어댑터 alarm 콜백 패턴)**:
- `RabbitMQTransport.__init__`에 `alarm_callback: Optional[Callable]` 파라미터 추가.
- `set_alarm_callback()` setter 추가 (늦은 주입 — daemon.py에서 AlarmEmitter 생성 후 바인딩).
- `_fire_alarm_callback()` 내부 헬퍼: callback None guard + 예외 삼킴.
- `publish_event()` retry 실패 시 `publish_unavailable_after_reconnect` 콜백 호출.
- `_publish_dlq()` DLQ 실패 시 `dlq_publish_failed` 콜백 호출.
**KD-2 (reset.py transport None 안전)**:
- `_emit_reset_alarm(alarm_emitter, code, title, fields)` 헬퍼: None guard + 예외 삼킴.
- `run_reset()` 시그니처에 `alarm_emitter: Optional[object] = None` 추가.
- migration 예외(raise 전)와 schema_ok=False 지점에서 헬퍼 호출.
**KD-3 (publish_alarm_event 별도 메서드)**:
- `trex.alerts` exchange, `mandatory=False`, `delivery_mode=1` (non-persistent).
- `MessagingTransport` Protocol + `RabbitMQTransport` + `InMemoryTransport` 모두 구현.
**KD-4 (AlarmEmitter 인터페이스)**:
- `AlarmEmitter(transport: Optional[MessagingTransport])`.
- `publish_alarm(code, title, *, fields=None, severity="critical")`.
- `source="trex-core"`, `schema_version="1"` 고정.
- `occurred_at` = KST ISO-8601.
## Execution Log
1. docs/interface-contract.md — trex.alerts 섹션 추가 (REQ-098 완료)
2. topology.py — ALERTS_EXCHANGE + alarm_routing_key() (REQ-099 일부)
3. messaging.py — publish_alarm_event Protocol 추가 (REQ-099 일부)
4. rabbitmq.py — declare_topology + publish_alarm_event + 콜백 패턴 (REQ-099 완료 + REQ-100 KD-1)
5. in_memory.py — publish_alarm_event stub (REQ-099 완료)
6. alarm_emitter.py — AlarmEmitter 신규 (REQ-100 핵심)
7. execution.py — fill_price_unresolvable + sell_skipped_no_balance (REQ-100 배선)
8. reconciliation.py — core_holding_broker_flat (REQ-100 배선)
9. ledger_outbox.py — ledger_delivery_failed (REQ-100 배선)
10. killswitch.py — kill_switch_fired (REQ-100 배선)
11. snapshot_scheduler.py — portfolio_hub_unavailable (REQ-100 배선)
12. reset.py — schema_verification_failed + migration_failed (REQ-100 배선)
13. daemon.py — AlarmEmitter 생성·주입 + 서비스 생성 순서 재배치 (REQ-100 조립)
14. runtime-flows.md — 알람 발신 흐름 섹션 추가 (REQ-101 완료)
15. 테스트 작성 (test_alarm_emitter.py 29개, topology/in_memory 추가)
## Validation
```
pytest tests/application/test_alarm_emitter.py -v
→ 29 passed, 0 failed, 0 skipped
pytest --tb=short -q (전체)
→ 608 passed, 47 skipped, 0 failed
ruff check trex_core/ tests/
→ All checks passed!
```
## Remaining Risks
- R-1(Low): `trex.alerts` exchange 선언은 실 RabbitMQ에서만 검증 가능 (단위에서 mock). 팀장이 실인프라 통합 테스트 수행 예정.
- R-2(Low): daemon.py에서 event_publisher가 `InMemoryTransport`인 경우 `set_alarm_callback()`이 없어 `hasattr` guard 처리됨 (InMemoryTransport는 콜백 불필요).
- R-3(Low): reset.py의 `alarm_emitter` 파라미터가 `main()`(CLI 진입점)에서 None으로 실행됨 — 의도된 동작이나 실인프라 CLI 시나리오에서 알람 미발행.
@@ -0,0 +1,52 @@
# BAT-049: 알람 시스템 — 알람 envelope wire 계약 + trex.alerts exchange + core CRITICAL emitter + baseline 정합 (dcy-033)
- Status: release-candidate
- Profile: standard
- Source Discovery: dcy-033
- Included REQ: REQ-098 (Interface), REQ-099 (Integration), REQ-100 (Integration), REQ-101 (Documentation)
- Excluded REQ: 없음
## Summary
- dcy-033(제품군 알람 전달 체계) confirmed Discovery에서 확정된 알람 시스템 1단계 구현.
- REQ-098: 알람 envelope 표준 JSON 스키마 wire 계약을 `docs/interface-contract.md`에 문서화.
- REQ-099: `trex.alerts` (topic, durable=True) AMQP exchange를 `declare_topology()`에 멱등 선언 추가.
- REQ-100: application 레이어 `AlarmEmitter` + CRITICAL code 8종 발생 지점 배선.
- REQ-101: `docs/runtime-flows.md`에 알람 발신 흐름 섹션 추가 + baseline 문서 정합.
## Scope
- In Scope:
- `docs/interface-contract.md` — `Interface: trex.alerts (알람 채널, dcy-033)` 신규 섹션 추가(기존 섹션 불변)
- `trex_core/adapters/messaging/topology.py` — `ALERTS_EXCHANGE` 상수 추가
- `trex_core/adapters/messaging/rabbitmq.py` — `declare_topology()`에 `trex.alerts` 선언, `publish_alarm_event()` 메서드 추가
- `trex_core/adapters/messaging/in_memory.py` — `publish_alarm_event()` stub 추가
- `trex_core/application/messaging.py` — `MessagingTransport` 프로토콜에 `publish_alarm_event()` 추가
- `trex_core/application/alarm_emitter.py` — `AlarmEmitter` 신규 구현
- `trex_core/application/execution.py` — `fill_price_unresolvable`, `sell_skipped_no_balance` 배선
- `trex_core/application/reconciliation.py` — `core_holding_broker_flat` 배선
- `trex_core/application/ledger_outbox.py` — `ledger_delivery_failed` 배선
- `trex_core/application/killswitch.py` — `kill_switch_fired` 배선
- `trex_core/adapters/broker/rabbitmq.py` — `publish_unavailable_after_reconnect` / `dlq_publish_failed` 배선 (콜백 패턴)
- `trex_core/application/portfolio.py` / `trex_core/application/snapshot_scheduler.py` — `portfolio_hub_unavailable` 배선
- `trex_core/runtime/reset.py` — `schema_verification_failed` / `migration_failed` 배선 (transport None 안전)
- `trex_core/runtime/daemon.py` — `AlarmEmitter` 생성·주입
- `tests/application/test_alarm_emitter.py` — AlarmEmitter 단위 테스트 신규
- `tests/adapters/messaging/test_topology.py` 또는 `test_in_memory_transport.py` — declare_topology trex.alerts assertion 추가
- `docs/runtime-flows.md` — 알람 발신 흐름 섹션 추가
- Out of Scope:
- trex.events / trex.intent.req / trex.slots.req / trex.control.req 변경 — 가산·격리 불변(REQ-099 AC-2)
- 텔레그램 봇 토큰·chat_id 등 외부 알람 시크릿 — trex-notifier 담당
- INFO·WARN code 배선 — CRITICAL 우선(dcy-033 범위 경계)
- 통합/e2e(실 RabbitMQ) 테스트 — 팀장이 실인프라로 수행
## Documents
- Planning: [planning.md](./planning.md)
- Design: [design.md](./design.md)
- Implementation: [implementation.md](./implementation.md)
- Verification: [verification.md](./verification.md)
## Notes
- 이 batch는 신규 AMQP exchange(`trex.alerts`) 추가이지만 기존 wire 계약(exchange/큐/라우팅)을 변경하지 않는 가산·격리 추가이므로 wire 계약 변경 에스컬레이션 대상이 아님.
- `publish_unavailable_after_reconnect` / `dlq_publish_failed`는 rabbitmq.py 어댑터에서 발생 → 콜백 패턴으로 상위에 전파. 설계 결정 details는 design.md 참조.
- `reset.py`는 transport 미연결 가능 → `AlarmEmitter`가 `None` transport를 안전하게 처리.
- R-1(Low): trex.alerts exchange 선언 후 notifier 미연결 시 알람 발행은 `mandatory=False`로 silent drop — 운영상 무해.
- R-2(Low): rabbitmq.py 어댑터 콜백 주입 시 생성 시점 의존성 순환 가능성 → daemon.py에서 생성 순서 주의.
@@ -0,0 +1,54 @@
# BAT-049 Planning
- Status: confirmed
- Batch: BAT-049
- Profile: standard
## Included REQ
- REQ-098: 알람 envelope wire 계약 정의 (Interface — 문서 전용)
- REQ-099: trex.alerts AMQP exchange 토폴로지 + 가산·격리 불변식 (Integration)
- REQ-100: trex-core 알람 emitter — CRITICAL code 발신 배선 (Integration)
- REQ-101: 알람 발신 흐름 baseline 문서 정합 (Documentation)
## Delivery Plan
REQ-098(문서), REQ-099(topology), REQ-100(emitter + 배선), REQ-101(문서 정합)은 단일 인과 사슬을 형성한다:
- REQ-098 wire 계약 문서가 선행 확정되어야 REQ-100 emitter 구현의 필드/타입 근거가 생긴다.
- REQ-099 topology 선언이 선행되어야 REQ-100 발행 경로가 확보된다.
- REQ-101은 REQ-098~100 구현이 완료된 후 baseline 문서를 정합한다.
구현 순서:
1. **REQ-098 먼저**: `docs/interface-contract.md`에 `Interface: trex.alerts (알람 채널, dcy-033)` 섹션 추가 (기존 섹션 불변). 이 문서가 이후 구현의 단일 출처.
2. **REQ-099**: topology 상수 + `declare_topology()` 확장 + `publish_alarm_event()` 포트/어댑터 추가.
3. **REQ-100**: `AlarmEmitter` 구현 + 8개 CRITICAL 발생 지점 배선. `rabbitmq.py` 콜백 설계 병행.
4. **REQ-101**: `docs/runtime-flows.md` 알람 발신 섹션 추가 + `docs/interface-contract.md` Change Log + Last Updated 갱신.
5. **테스트**: AlarmEmitter 단위 테스트(8 code × 2 케이스) + declare_topology assertion + 기존 테스트 회귀.
## Dependencies
- BAT-047(trex.events 재연결 신뢰성): 완료(release-candidate) — `RabbitMQTransport.publish_event()` 재시도 패턴을 `publish_alarm_event()`에서 참조.
- BAT-048(체결가 백필/알람 스파이크): 완료 — REQ-097 스파이크에서 trex.alerts AMQP 채널이 최우선 채택됨.
- `MessagingTransport` 포트 (BAT-007): `publish_alarm_event()` 메서드 추가로 확장. 기존 메서드 시그니처 불변.
## Design Gate
**Design 필요: yes (standard — application 레이어 신규 컴포넌트 + 어댑터 확장 + 의존성 역전 설계 결정)**
- `publish_unavailable_after_reconnect` / `dlq_publish_failed`가 어댑터(`rabbitmq.py`)에서 발생하는데 `AlarmEmitter`는 application에 있다 → 콜백 패턴 vs 이벤트 버스 선택을 design에서 확정해야 함.
- `reset.py`(CLI 진입점)에서 transport 미연결 가능 → None-safe 처리 방식 설계 필요.
- `MessagingTransport` 프로토콜 확장이 기존 `InMemoryTransport` 호환성을 유지하는지 확인 필요.
## Milestones
1. REQ-098 문서 완성 (interface-contract.md 신규 섹션)
2. REQ-099 topology + publish_alarm_event 포트/어댑터 구현
3. REQ-100 AlarmEmitter + 8개 배선 완성
4. REQ-101 baseline 문서 정합
5. 단위 테스트 전부 통과 + ruff clean
## Risks
- R-1(Low): `publish_alarm_event` 시그니처 추가 시 `InMemoryTransport`에 stub 구현 누락 → Protocol mismatch. 완화: design 에서 확인 + test에서 isinstance 체크 추가.
- R-2(Low): rabbitmq.py 어댑터 콜백 주입 시 daemon.py의 생성 순서 순환 가능성. 완화: design에서 콜백 늦은 주입(setter) 패턴 채택.
- R-3(Low): 기존 단위 테스트 InMemoryTransport 사용분이 declare_topology 호출 없이 통과해야 함. 완화: in_memory declare_topology는 no-op 유지.
@@ -0,0 +1,102 @@
# BAT-049 Verification
- Status: confirmed
- Ready for Release: true
## Human Approval Memo
- Approved by: 팀장
- Approved at: 2026-06-21 (KST)
- Approved scope: BAT-049 confirm-batch-verification 승인 — QC PASS-WITH-NOTES (blocker 0) + 팀장 실인프라 e2e 통과
- Residual risk accepted:
- reset.py 2 code(`schema_verification_failed`/`migration_failed`) 간접 검증(기존 reset 테스트 회귀) — 직접 AC 매핑 minor 갭. 정직 공개됨.
- 배포 시 운영 AMQP 설정은 코드 아닌 ops 항목.
- Approval rationale:
- 단위: 608 passed / 47 skipped / 0 fail. 신규 AlarmEmitter 29 테스트 포함. ruff clean.
- 독립 opus QC=PASS-WITH-NOTES (blocker 0): best-effort 비차단 · 서비스 생성 순서 재배치 안전 · 가산격리(기존 exchange/큐 불변) · domain 순수성 · 시크릿 0 확인.
- 팀장 실인프라 e2e(2026-06-21): 실 RabbitMQ에 `trex.alerts`(topic,durable) 선언·발행(`alarm.critical.trex-core`) → trex-notifier 수신 → CRITICAL 분류 → 텔레그램 sendMessage HTTP 200. 기존 토폴로지 불변 확인.
- Batch: BAT-049
## Test Execution Results
```
pytest tests/application/test_alarm_emitter.py -v
→ 29 passed, 0 failed, 0 skipped
pytest --tb=short -q (전체 스위트)
→ 608 passed, 47 skipped, 0 failed
ruff check trex_core/ tests/
→ All checks passed!
```
실행 환경: Python 3.12.3, pytest 8.4.2, 2026-06-21
## Acceptance Criteria Mapping
### REQ-098: 알람 envelope wire 계약 정의
| AC | 검증 방법 | 결과 |
|---|---|---|
| AC-1: `docs/interface-contract.md`에 `Interface: trex.alerts` 섹션이 7개 필드 포함 | 수동 확인 | PASS — 섹션 추가됨, 7필드 명시 |
| AC-2: severity 값이 3 리터럴 제한 + schema_version="1" 명시 | 수동 확인 | PASS — 명시됨 |
| AC-3: 기존 섹션 내용 불변 (diff=신규 섹션 추가뿐) | 수동 확인 (기존 텍스트 비교) | PASS — 기존 섹션 변경 없음 |
| AC-4: routing key `alarm.{severity}.{source}` 형식 명시 | 수동 확인 | PASS — 섹션에 명시됨 |
| AC-5: Update Trigger 포함 | 수동 확인 | PASS — `Update Trigger (알람 envelope 스키마)` 서브섹션 존재 |
| AC-6: 다른 모듈 개발자가 `trex_core` import 없이 envelope 정보 획득 가능 | 수동 확인 | PASS — 섹션에 모든 필드·타입·routing key 포함 |
### REQ-099: trex.alerts AMQP exchange 토폴로지 + 가산·격리 불변식
| AC | 검증 방법 | 결과 |
|---|---|---|
| AC-1: `declare_topology()` 호출 시 `trex.alerts`(topic, durable=True) 선언 | `test_alerts_exchange_constant` + `test_alarm_routing_key_format` (topology 상수 검증) + rabbitmq.py 코드 확인 | PASS (단위) / 미실행 (실 RabbitMQ 통합) |
| AC-2: 기존 exchange/큐 type·durable·인수 불변 | 기존 테스트 스위트 608 통과 (회귀 없음) | PASS |
| AC-3: mandatory=False → unroutable 시 silent success | `publish_alarm_event()` 구현 + `test_publish_alarm_event_records_event` (InMemory) | PASS (단위) / 미실행 (실 RabbitMQ 통합) |
| AC-4: interface-contract.md 토폴로지 명세(exchange명/타입/durable/routing key/발행자/소비자) | 수동 확인 | PASS — 섹션에 모두 명시 |
| AC-5: 기존 topology 단위 테스트 + 신규 trex.alerts assertion 통과 | `test_topology.py` 5개 테스트 (신규 2개 포함) 통과 | PASS |
| AC-6: transport disconnect 시 알람 silently skip | `test_publish_alarm_transport_unavailable_no_reraise` 8 code × PASS | PASS |
### REQ-100: trex-core 알람 emitter — CRITICAL code 발신 배선
| AC | 검증 방법 | 결과 |
|---|---|---|
| AC-1: 8개 code 각각 올바른 envelope (`source/severity/schema_version`) | `test_publish_alarm_envelope_fields` 8 code parametrize | PASS |
| AC-2: TransportUnavailableError 시 re-raise 없음 + ERROR 로그 | `test_publish_alarm_transport_unavailable_no_reraise` 8 code | PASS |
| AC-3: 임의 예외 시 re-raise 없음 + ERROR 로그 | `test_publish_alarm_arbitrary_exception_swallowed` 8 code | PASS |
| AC-4: 알람 발행 실패가 core 본업 경로 비차단 | 코드 확인: try/except 삼킴 패턴, 기존 execution/killswitch/ledger/reconcile 테스트 모두 통과 | PASS |
| AC-5: TELEGRAM_* 참조 0건 | `test_alarm_emitter_no_telegram_reference` + `grep -r TELEGRAM trex_core/` → 0건 | PASS |
| AC-6: 8 code × 2 케이스 (정상 + 예외) 모두 통과 | 29개 test 통과 | PASS |
| AC-7: pika 직접 import 없음 | `test_alarm_emitter_no_pika_import` | PASS |
| AC-8: domain 레이어 messaging import 없음 | `test_domain_import_sweep.py` 기존 통과 유지 | PASS |
### REQ-101: 알람 발신 흐름 baseline 문서 정합
| AC | 검증 방법 | 결과 |
|---|---|---|
| AC-1: `docs/runtime-flows.md`에 `Flow: 알람 발신 (trex.alerts, dcy-033)` 섹션 존재 | 수동 확인 | PASS |
| AC-2: AlarmEmitter → MessagingTransport → trex.alerts 흐름 + best-effort 정책 명시 | 수동 확인 | PASS |
| AC-3: CRITICAL code 8종 + 발생 모듈 경로 기술 | 수동 확인 (표 형식으로 10개 항목 포함 — schema_verification_failed/migration_failed 별도 행) | PASS |
| AC-4: trex.events와의 차이(독립 exchange, mandatory=False, DLQ 없음) 명시 | 수동 확인 (비교 표 포함) | PASS |
| AC-5: `docs/interface-contract.md` Change Log에 dcy-033 항목 기록 | 수동 확인 | PASS |
| AC-6: 두 문서 `Last Updated (KST)` 갱신 | 수동 확인 (2026-06-21 BAT-049) | PASS |
## Regression Evidence
- 기존 608개 테스트 전부 통과 (47 skipped 유지 — DB/Redis/실RabbitMQ 의존 테스트)
- `tests/domain/test_domain_import_sweep.py` — domain 순수성 불변식 통과
- execution/reconciliation/killswitch/ledger_outbox 관련 기존 테스트 전부 통과
## Missing Evidence (통합 — 팀장 실인프라 수행)
1. **실 RabbitMQ `trex.alerts` exchange 선언**: `declare_topology()` 호출 후 RabbitMQ 관리 콘솔에서 exchange 존재·속성(topic, durable=True) 확인.
2. **실 RabbitMQ mandatory=False 발행**: notifier 큐 미바인딩 상태에서 `publish_alarm_event()` 호출 → 예외 없이 성공 확인.
3. **실 RabbitMQ 기존 topology 불변**: 기존 exchange/큐 목록이 변경 없이 유지되는지 확인.
## Blocking Issues
없음 (단위 테스트 전부 통과, 통합은 팀장 실인프라 수행으로 위임됨).
## Notes
- 이 verification은 단위 테스트 결과와 코드 수동 검토만 포함한다. 실인프라 통합 테스트는 `Missing Evidence` 3건으로 명시했으며 confirm-batch-verification 전에 팀장이 수행한다.
- `schema_verification_failed` / `migration_failed`는 ALARM-INVENTORY.md §A에 단일 항목(`schema_verification_failed / migration_failed`)으로 기재되어 있으나, 구현에서는 두 개별 code로 분리 배선하고 테스트에서는 `schema_verification_failed`와 `migration_failed` 대신 `CRITICAL_CODES` 리스트 8개만 parametrize했다. reset.py 배선은 `test_reset.py` 기존 테스트 회귀로 간접 검증.
+1
View File
@@ -56,3 +56,4 @@
| BAT-046 | standard | released | REQ-086, REQ-088, REQ-089, REQ-090 | dcy-029 (issue #29, P1) | [bat-046](./bat-046_20260616_observability-classification-pnl-runbook/) |
| BAT-047 | standard | release-candidate | REQ-091, REQ-092, REQ-093, REQ-094 | dcy-031 | [bat-047](./bat-047_20260619_event-publisher-connection-reliability/) |
| BAT-048 | standard | in-delivery | REQ-095, REQ-096, REQ-097 | dcy-032 | [bat-048](./bat-048_20260621_fill-price-backfill-guard-logging-alert-spike/) |
| BAT-049 | standard | release-candidate | REQ-098, REQ-099, REQ-100, REQ-101 | dcy-033 | [bat-049](./bat-049_20260621_alarm-system-dcy-033/) |