docs(proofline): restore line-0008 canonical approved artifacts verbatim

Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-08-26 05:29:56 +00:00
committed by GitHub
co-authored by genichin
parent 75406af970
commit ac3f27241a
3 changed files with 82 additions and 54 deletions
+44 -13
View File
@@ -3,28 +3,59 @@ id: "dcy-0008"
status: confirmed
---
# 최초 `reconcile_warned` intent 거부 알림
# Reconcile-warned strategy intent rejection alerts
## Problem
`IntentService`는 재조정 경고(`reconcile_warned`)가 남아 있는 slot의 전략 buy/sell intent를 거부하지만, 현재 경고 알림을 발행하지 않아 운영자가 최초 거부를 즉시 인지하기 어렵다. 기존 intent dedup 기준(`IntentStore.try_insert/get/finalize`)과 차단 정책은 유지되어야 하며, 같은 `intent_id` 재시도에서는 추가 알림이 없어야 한다.
활성 reconciliation warning이 있는 slot에 외부 strategy가 신규 buy 또는 sell intent를 제출하면 `IntentService`는 `reconcile_warned` 사유로 안전하게 거부하지만, 현재는 RPC 응답과 `intent_log` 외에 운영자에게 즉시 전달되는 신호가 없다. Strategy가 마지막 slots RPC 이후 발생한 warning을 알지 못한 채 intent를 제출하는 race에서도 거부 자체는 정상 동작하지만, 운영자는 반복되는 거부와 전략 정체의 원인을 별도 조사 전까지 인지하기 어렵다.
기대 상태는 durable intent idempotency와 기존 reconciliation 차단 권한을 유지하면서, 새 `intent_id`가 최초로 `reconcile_warned` 최종 거부된 시점에만 allowlist 기반 `trex.alerts` 경고를 best-effort로 한 건 발행하는 것이다. 동일 `intent_id`의 retry는 저장된 결정을 그대로 반환하되 추가 경고를 만들지 않아야 하며, 알림 실패가 intent 거부·응답·저장 또는 다른 business path를 바꾸어서는 안 된다.
## Evidence
- `trex_core/application/intents.py`는 `reconcile_warned` 거부 결과를 `IntentStore.finalize()`로 저장하지만 `trex.alerts` 알림 발행이 없다.
- daemon은 공용 `AlarmEmitter`를 이미 생성해 다른 서비스에 주입하지만 `IntentService`에는 연결하지 않는다.
- issue #74와 승인된 REQ-0008/AC-0025는 finalize 이후 최초 거부 1회 알림, allowlist payload, optional context 조회 실패/발행 실패의 비차단 경계를 요구한다.
- GitHub issue [#74](https://github.com/fourblocks2020/trex-core/issues/74)는 실운영에서 warning 활성 뒤 strategy intent가 거부됐지만 즉시 알림이 없어 원인 파악이 지연된 사례를 기록하고, 최초 `reconcile_warned` 거부 알림·중복 억제·민감정보 제외·non-blocking 동작을 요구한다. 확인 시 issue는 open이고 후속 comment나 연결된 PR은 없었다.
- 현재 `trex_core/application/intents.py`의 `submit_buy()`와 `submit_sell()`은 `IntentStore.try_insert()`를 dedup gate로 호출한 뒤 admission 결과를 `finalize()`하고 반환한다. Duplicate `intent_id`는 `_dedup()`에서 기존 결정을 반환하므로 새 admission을 실행하지 않는다.
- 같은 파일의 buy·sell warning gate는 `warn_store.is_warned(slot_id)`가 참이면 `IntentDecision(status="rejected", reason="reconcile_warned")`을 반환한다. `IntentService` constructor에는 `alarm_emitter` 의존성이 없고, 이 최종 거부를 `trex.alerts`에 발행하는 호출도 없다.
- `trex_core/runtime/daemon.py`는 하나의 `AlarmEmitter`를 생성해 ledger, kill-switch, scheduler와 execution service에 주입하지만 `IntentService`에는 주입하지 않는다. 따라서 production intent path에는 재사용 가능한 publisher가 존재하지만 wiring이 빠져 있다.
- `trex_core/application/alarm_emitter.py`의 `publish_alarm()`은 `severity`로 `critical | warn | info`를 사용하고 publish exception을 삼켜 호출자의 business path를 중단하지 않는 best-effort 계약이다. `docs/interface-contract.md`도 warning wire literal을 `warn`으로 정의한다.
- `trex_core/adapters/persistence/postgresql.py`의 `PostgreSQLIntentStore`는 `intent_id` unique insert를 durable dedup gate로 사용하고 final `status`, `slot_id`, `reason`을 저장한다. 별도 notification state 없이도 “최초 처리된 intent만 발행 후보”라는 경계는 이 기존 gate로 정할 수 있다.
- `tests/application/test_reconcile_warning_blocking.py`와 `tests/application/test_slots_handler_quantity_mismatch.py`는 buy/sell `reconcile_warned` 거부와 last-RPC race를 검증하지만 alarm 발행을 단언하지 않는다. `tests/application/test_messaging_intent_consumer.py`는 duplicate `intent_id`가 저장된 동일 결정을 반환하는 idempotency를 검증한다.
- 현재 `main` revision `2e314232e3073381b6fc9ee268ae8e12fbcc8749`에서 관련 baseline인 `tests/application/test_reconcile_warning_blocking.py`, `tests/application/test_slots_handler_quantity_mismatch.py`, `tests/application/test_messaging_intent_consumer.py`, `tests/application/test_alarm_emitter.py`를 실행한 결과 `84 passed`였다.
- Git history에서 `c193388`은 external strategy buy/sell에 warning gate를 의도적으로 추가하면서 safety-sell·kill-switch·watchdog가 `IntentService`를 우회하도록 정했다. 이번 Line은 그 차단 정책이나 내부 안전 경로를 변경하지 않고, 이미 확정된 외부 intent 거부의 운영 가시성만 추가한다.
## Scope
- 대상은 외부 전략 buy/sell intent의 `rejected/reconcile_warned` 결과다.
- `IntentStore.finalize()` 성공 뒤에만 `severity=warn`, `code=strategy_intent_rejected` 알림을 best-effort로 발행한다.
- 필수 fields(`instance_key`, `intent_id`, `intent_type`, `reason`, `broker_submitted=false`)와 확인된 optional fields(`slot_id`, `portfolio_id`, `symbol`, `strategy_id`)만 포함한다.
- 같은 `intent_id`는 기존 durable dedup 결과를 재사용해 추가 발행하지 않고, 다른 `intent_id`는 독립 발행한다.
- 발행기 부재/실패/optional context 조회 실패는 저장 결과와 RPC 응답을 변경하지 않는다.
- AC admission은 새 `AC-0025` **create**로 분류한다. 가장 가까운 `AC-0023`은 C-case strategy RPC 제외·intent blocking·warning clear 권한을 소유하고, `AC-0024`는 B/C mismatch 자체의 detected/changed/resolved lifecycle을 소유한다. 이번 동작은 “외부 strategy intent가 실제로 차단된 최초 사건”이라는 별도 운영 신호이며 독립 PASS/FAIL과 장기 active 가치가 있으므로 기존 AC의 update 또는 satisfy로 흡수하지 않는다. `update`, `retire`, `satisfy`, `release evidence` 대상은 없다.
- `IntentService`의 buy·sell 처리에서 새 `intent_id`의 최종 decision이 `status="rejected"` 및 `reason="reconcile_warned"`로 확정되고 `IntentStore.finalize()`가 성공한 뒤에만 `AlarmEmitter.publish_alarm()`을 한 번 호출한다.
- Alarm은 기존 wire 계약에 맞춰 `severity="warn"`, code `strategy_intent_rejected`를 사용한다. Fields는 최소 allowlist인 `instance_key`, `intent_id`, `intent_type`, `reason`, `broker_submitted=false`를 포함하고, 요청 또는 canonical slot에서 안전하게 확인 가능한 경우에만 `slot_id`, `portfolio_id`, `symbol`, normalized `strategy_id`를 추가한다.
- Buy의 canonical `slot_id` 입력 또는 legacy `portfolio_id` mapping과 sell의 `slot_id` 입력 모두에서 가능한 slot context를 수집하되, context 조회 실패나 optional field 부재는 원래 `reconcile_warned` decision을 바꾸거나 알림을 위해 새로운 mutation을 유발하지 않는다.
- 동일 `intent_id`의 duplicate/retry는 기존 durable `intent_log` 결정을 반환하고 alarm을 재발행하지 않는다. 최초 처리 중 관찰되는 `processing`/`duplicate_in_flight` 응답도 별도 alarm 대상이 아니다.
- Alarm publish 성공 여부와 무관하게 원래 거부 decision과 RPC 응답은 유지된다. Publisher 부재와 transport failure는 non-blocking이며 별도 durable outbox, notification-state row 또는 retry loop를 만들지 않는다.
- Alarm title·fields·rendered log에는 credential, token, account number/hash, DSN, raw strategy payload, raw broker/provider payload와 raw exception body를 포함하지 않는다.
- Production daemon은 이미 생성하는 동일 `AlarmEmitter`를 `IntentService`에 주입한다. Optional dependency를 사용하는 직접 application test와 non-daemon construction은 기존 동작을 유지한다.
- Verification은 buy와 sell의 최초 경고 각 한 건, 같은 `intent_id` retry의 추가 경고 0건, 다른 `intent_id`의 독립 경고, admitted 및 다른 rejection reason의 무발행, publish failure의 non-blocking, payload allowlist·`broker_submitted=false`, production wiring과 기존 warning blocking/idempotency regression을 포함한다.
## Out of Scope
- AC-0023/AC-0024의 warning 판정·clear authority 변경
- broker 주문/취소, slot/warning/ledger/portfolio-hub write side effect 추가
- 신규 durable outbox, publish retry persistence, restart 후 보장 재발행
- `AC-0023`의 B/C warning 판정, slots RPC 제외, buy/sell 차단, fail-closed 및 authoritative clear semantics 변경.
- `AC-0024`의 mismatch detected/changed/resolved alarm과 `reconcile_notification_state` 변경 또는 통합.
- Strategy retry·backoff·pause·circuit-breaker 정책, strategy 또는 TVR wire schema 변경, 새로운 broker order submit/cancel 동작.
- Safety-sell, kill-switch, watchdog 등 `IntentService`를 우회하는 내부 안전 경로의 차단 또는 알림.
- 다른 rejection reason에 대한 범용 intent rejection alarm, 알림 집계·rate limit·severity escalation.
- Alarm delivery를 위한 durable outbox, 별도 notification table, 재시작 후 재발행 또는 publish retry 보장. `finalize()` 성공과 alarm publish 사이 process crash로 인한 알림 유실 가능성은 이 Line에서 해소하지 않는다.
- `trex-notifier`의 message formatting·routing·delivery channel 변경과 운영 배포 자체.
## Risks and Unknowns
### Risks
- `IntentStore.finalize()` 뒤 best-effort publish를 수행하므로 process가 그 사이 종료되거나 transport가 실패하면 durable decision은 남지만 alarm은 유실된다. Issue가 outbox/retry를 제외했으므로 이 Line은 business path 독립성을 우선하며, delivery 보장이 필요해지면 별도 Requirement로 다룬다.
- Buy는 canonical `slot_id`와 legacy `portfolio_id` 두 입력을 허용하고 sell은 `sell_strategy_id`를 사용한다. Context 정규화가 admission logic을 복제하면 drift 위험이 있으므로, 알림은 decision에 영향을 주지 않는 helper와 allowlist output으로 제한하고 양쪽 입력 형태를 테스트해야 한다.
- 기존 `AlarmEmitter`는 여러 subsystem이 공유한다. Intent-specific 실패 처리나 logging 변경이 공용 emitter 계약을 확장하지 않도록 production wiring과 publisher failure test를 분리해야 한다.
- Rejection payload에 raw request 또는 `extra` dict를 복사하면 민감정보가 유출될 수 있다. 허용된 scalar field만 명시적으로 구성하고 forbidden key/value sentinel을 rendered payload와 log에서 검사해야 한다.
### Assumptions
- Issue #74의 “경고(WARNING)”는 현재 interface contract의 wire literal `severity="warn"`을 의미한다.
- “최초”의 canonical identity는 기존 `IntentStore.try_insert()`가 소유하는 `intent_id`이며, process-local set이나 새로운 persistence를 추가하지 않는다.
- `strategy_intent_rejected`는 issue가 제시한 이 기능의 canonical alarm code로 사용하며, notifier-side inventory 등록이 별도로 필요하더라도 core의 event contract를 변경하지 않는다.
+27 -33
View File
@@ -10,48 +10,42 @@ criteria:
satisfy: []
---
# 최초 `reconcile_warned` intent 거부 알림 발행
# Reconcile-warned strategy intent rejection alerts
## Objective
`trex-core`는 재조정 경고가 남아 있는 slot에 대한 외부 전략 buy/sell intent가 `rejected/reconcile_warned`로 확정될 때, 기존 idempotency와 차단 정책을 유지하면서 새 `intent_id`의 최초 거부에 한해 `trex.alerts` 경고를 한 건 발행해야 한다.
`trex-core`가 활성 reconciliation warning 때문에 외부 strategy의 신규 buy 또는 sell intent를 `reconcile_warned`로 최종 거부하면, 해당 `intent_id`의 최초 처리에서만 원인을 식별할 수 있는 sanitized warning event를 `trex.alerts`에 한 건 발행하게 한다. 기존 durable intent idempotency와 warning 차단 권한을 유지하고, duplicate intent 및 무관한 decision의 알림을 억제하며, 알림 실패가 intent decision·저장·응답 또는 broker 동작에 영향을 주지 않도록 한다.
## Scope
- 경고 발행 조건
- `IntentStore.try_insert()`가 성공한 최초 처리여야 한다.
- 외부 전략 buy 또는 sell intent여야 한다.
- 최종 결과가 `status="rejected"`, `reason="reconcile_warned"`여야 한다.
- `IntentStore.finalize()`가 성공한 뒤여야 한다.
- 경고 계약
- `severity=warn`
- `code=strategy_intent_rejected`
- dedup 경계
- 같은 `intent_id` 재요청은 저장된 기존 결과를 반환하고 경고를 재발행하지 않는다.
- `duplicate_in_flight`와 최초 처리 중 중복 요청은 경고 대상이 아니다.
- 같은 slot이라도 `intent_id`가 다르면 독립 발행한다.
- process-local set, 별도 notification table, `reconcile_notification_state`를 추가하지 않는다.
- payload allowlist
- 필수: `instance_key`, `intent_id`, `intent_type`, `reason="reconcile_warned"`, `broker_submitted=false`
- optional(확인 가능 시에만): `slot_id`, `portfolio_id`, `symbol`, `strategy_id`
- buy `strategy_id`와 sell `sell_strategy_id`는 공통 `strategy_id`로 표현한다.
- `quantity`, `entry_price`, `limit_price`, `venue`, raw request/strategy `extra`는 포함하지 않는다.
- failure isolation
- emitter 부재/실패, optional context 조회 실패는 저장 결과와 RPC 응답을 바꾸지 않는다.
- 이미 확정된 거부를 rollback하거나 admitted로 바꾸지 않는다.
- production wiring
- `IntentService`는 optional `AlarmEmitter`를 받는다.
- daemon은 기존 공용 `AlarmEmitter`를 `IntentService`에 전달한다.
- emitter 없이 생성하는 기존 테스트/사용 경로는 계속 동작한다.
- `IntentService`가 새 `intent_id`를 처리하여 buy 또는 sell decision을 `status="rejected"`, `reason="reconcile_warned"`로 결정하고 `IntentStore.finalize()`가 성공한 뒤에만 warning event를 발행한다.
- 동일 `intent_id`의 duplicate/retry는 기존 `IntentStore.try_insert()`와 저장된 final decision을 canonical dedup authority로 사용한다. Duplicate path, 처리 중인 intent 및 process-local cache는 새 event를 만들지 않는다.
- 서로 다른 `intent_id`가 같은 warned slot을 대상으로 각각 거부되면 각 intent는 독립적인 최초 거부 event 한 건을 만든다.
- Event는 기존 `AlarmEmitter`와 `trex.alerts` envelope·routing contract를 재사용하고 `severity="warn"`, `code="strategy_intent_rejected"`를 사용한다.
- Event fields의 필수 allowlist는 `instance_key`, `intent_id`, `intent_type`, `reason="reconcile_warned"`, `broker_submitted=false`다. Canonical slot 또는 intent에서 안전하게 확인된 값이 있을 때만 `slot_id`, `portfolio_id`, `symbol`, `strategy_id`를 추가하며, unavailable optional field는 추정하거나 raw payload에서 복사하지 않고 생략한다.
- Buy의 `strategy_id`와 sell의 `sell_strategy_id`는 event의 공통 `strategy_id` field로 정규화한다. Buy의 canonical `slot_id` 또는 legacy `portfolio_id` 입력과 sell의 `slot_id` 입력 모두에서 같은 allowlist 계약을 적용한다.
- Optional context 조회 실패, publisher 부재 및 alarm transport failure는 원래 final decision과 RPC response를 변경하거나 intent 처리를 실패시키지 않는다. 알림을 위해 slot, warning, intent, broker, ledger 또는 portfolio-hub 상태를 mutation하지 않는다.
- Production daemon은 이미 생성하는 동일 `AlarmEmitter`를 `IntentService`에 연결한다. 직접 application construction과 non-daemon entry point는 optional publisher 경계에서 기존 동작과 호환된다.
- Buy·sell 최초 거부, duplicate 억제, 서로 다른 intent의 독립 발행, 다른 decision의 무발행, publish failure, optional context, payload 보안과 production wiring을 repository-local automated tests로 검증한다.
## Constraints
- AC-0023·AC-0024의 기존 warning 판정, clear authority, intent dedup semantics를 변경하지 않는다.
- 경고 발행을 위해 broker/slot/warning/ledger/portfolio-hub side effect를 추가하지 않는다.
- 경고/로그에 account number/hash, credential/token/password/secret, DSN/connection string, raw payload, exception 본문을 노출하지 않는다.
- AC-0023이 소유하는 B/C warning 판정, strategy RPC 제외, buy/sell `reconcile_warned` 차단, warn-store fail-closed 및 authoritative clear semantics를 완화·대체·확장하지 않는다.
- AC-0024의 mismatch detected/changed/resolved lifecycle, fingerprint, `reconcile_notification_state`와 operator `sync`/`verify` recovery authority를 변경하거나 재사용하지 않는다.
- Alarm은 `IntentStore.finalize()`보다 먼저 발행해서는 안 된다. Finalize가 실패하거나 final decision이 확정되지 않은 경우 알림을 성공한 거부로 보고해서는 안 된다.
- Publisher return value나 failure는 저장된 rejection을 rollback하거나 RPC status/reason을 바꾸지 않는다. 별도 durable outbox, notification-state persistence 또는 retry loop를 추가하지 않는다.
- Event title, fields와 이 경로가 남기는 rendered log에는 account number/hash, credential, token, password, DSN, raw strategy `extra`, raw request, raw KIS/broker/portfolio-hub payload 또는 raw exception body가 없어야 한다.
- `broker_submitted=false`는 거부가 broker submission 전에 확정됐음을 나타내며, 이 기능이 broker submit/cancel 호출을 새로 만들거나 기존 호출 순서를 바꾸어서는 안 된다.
- Safety-sell, kill-switch, watchdog처럼 `IntentService`를 우회하는 내부 안전 경로는 계속 reconciliation warning gate와 이 strategy rejection alarm의 대상이 아니다.
- 기존 intent RPC response schema, intent idempotency, slot transition과 alarm wire compatibility를 유지하며 RabbitMQ topology 또는 notifier rendering 변경을 요구하지 않는다.
- Normative Design Documents: 없음. 이 REQ와 AC-0025가 normative specification을 소유한다.
## Non-Goals
- 다른 거부 사유를 위한 범용 경고 정책 추가
- safety-sell/kill-switch/watchdog 내부 경로 신규 알림
- notifier 렌더링/채널, RabbitMQ topology, production 배포/릴리즈 절차 변경
- 다른 rejection reason의 범용 strategy intent alarm, reason별 severity 정책, rate limit·aggregation·escalation
- Strategy retry·backoff·pause·circuit-breaker 정책과 strategy/TVR wire schema 변경
- B/C mismatch 탐지·해소 알림과 strategy intent 거부 알림의 lifecycle 또는 persistence 통합
- Alarm의 exactly-once delivery, process crash 복구, durable outbox, publish retry와 재시작 후 재발행
- Warning 자동 해제, quantity 자동 보정, broker order submit/cancel, ledger write 또는 portfolio-hub write
- Safety-sell·kill-switch·watchdog 내부 경로의 신규 알림
- `trex-notifier` message formatting·delivery channel, RabbitMQ topology, production deployment, release 또는 issue closure