docs: add BAT-057 operator broker truth design

This commit is contained in:
Hermes Agent
2026-07-15 07:33:26 +09:00
parent c1e67c7055
commit 1b63e79edc
6 changed files with 400 additions and 0 deletions
@@ -0,0 +1,277 @@
# BAT-057 Design: operator broker truth 및 managed divergence emergency hotfix
- Status: confirmed
- Batch: BAT-057
- Design scope: REQ-119, REQ-120, REQ-121, REQ-122
- Implementation authorization: 본 문서는 구현 설계만 정의한다. `sync --live`, production target 선택, credential/secret 접근 및 서비스 실행은 범위 밖이다.
## Architecture Summary
### 1. 설계 목표와 현재 결함
현재 daemon은 `runtime.daemon._build_broker(config)`에서 env/TOML `KISConfig`로 broker를 먼저 만들고, `SnapshotScheduler._maybe_fetch_credential()` 성공 후에만 동일 객체의 `reinitialize()`를 호출한다. 반면 CLI `sync`/`verify`도 `_build_broker(cfg)`를 직접 호출하므로 fetched mode에서도 operator read가 env/TOML source로 시작한다. 또한 `KISBroker.get_balance()`은 token/HTTP/JSON/KIS `rt_cd` 오류를 `[]`로 변환하여 실제 empty balance와 구별하지 못한다. 결과적으로 operator가 잘못된 source 또는 실패를 `broker_qty_zero`로 해석하고 write/skip/성공 판단으로 이어갈 수 있다.
본 설계는 다음을 고정한다.
1. daemon과 모든 operator broker-read (`sync`, `verify`, 이후 같은 provider를 쓰는 read path)가 하나의 credential-resolution 및 broker-construction 경계를 사용한다.
2. fetched mode의 credential fetch 또는 balance read 실패는 명시적 break-glass가 없는 한 **broker가 없는 실패**로 종료한다. env/config으로 조용히 fallback하거나 empty holdings로 바꾸지 않는다.
3. `get_balance()` 성공 empty와 실패를 typed/sanitized result로 분리하고, 실패 결과가 reconciliation, `sync`, `verify`의 write/clear/skip 판단에 도달하지 못하게 한다.
4. unmatched broker holding만 instance-scoped managed-slot 존재 여부를 조회하여 true unmanaged와 managed non-holding divergence를 additive하게 분류한다.
## Changed Areas
### 2. 모듈별 변경과 소유권
| 모듈 | 변경/소유권 | 구현 경계 |
| --- | --- | --- |
| `trex_core/application/broker_provider.py` (신규) | `BrokerProvider` protocol, source metadata, credential resolution, broker reuse/invalidation policy의 application ownership | portfolio-hub read port와 KIS/paper construction을 조립하되 CLI 출력/DB write를 소유하지 않는다. |
| `trex_core/runtime/broker_factory.py` (신규) 또는 `runtime/daemon.py`의 명시적 factory | `DaemonConfig` → provider assembly. 기존 `_build_broker` 직접 사용자를 provider로 이관 | runtime composition만 담당하며 credential 값을 로그/JSON으로 만들지 않는다. 구현 시 순환 import가 생기면 factory를 별도 runtime module로 둔다. |
| `trex_core/runtime/daemon.py` | daemon은 process-lifetime provider에서 broker를 획득하고 scheduler에 같은 provider/reload callback을 주입 | daemon이 별도 env broker를 만들거나 scheduler와 다른 credential source를 해석하지 않는다. |
| `trex_core/application/snapshot_scheduler.py` | day-start fetched credential 성공/실패 및 broker reload를 provider contract로 호출 | scheduler의 entry gate 역할은 유지하되, fetched failure 뒤 env credential으로 trading/broker-read를 계속하는 현재 fallback은 제거한다. |
| `trex_core/runtime/cli.py` | `--config`가 있으면 daemon과 동일한 `DaemonConfig.from_config(toml_path, env_file=...)`로 instance TOML을 로드하고, `_cmd_sync`/`_cmd_verify`가 그 config의 같은 provider를 호출 | broker read가 성공하기 전 slot/warning write, `broker_qty_zero`, verify success를 만들지 않으며 CLI가 TOML `credential_fetch_enabled`를 env-only default로 덮어쓰지 않는다. |
| `trex_core/adapters/broker/kis.py` | typed sanitized `BrokerBalanceError`, strict `get_balance()`, token/error mapping | credential/token/header/raw account/payload를 error message, log, CLI JSON에 싣지 않는다. |
| `trex_core/application/reconciliation.py` | balance failure 전파와 managed non-holding decision 분기 | REQ-121 분류 자체로 slot/warning persistence를 쓰거나 상태를 바꾸지 않는다. 기존 A/B/C behavior는 strict balance 성공 뒤에만 유지한다. |
| `trex_core/domain/slot/repository.py` | managed-slot representative lookup protocol 추가 | query는 `instance_key`와 symbol만 입력으로 받고 status/retired 여부를 임의로 제외하지 않으며, safe triage context를 위한 deterministic `Slot` 한 건 또는 `None`을 반환한다. |
| `trex_core/domain/slot/in_memory_repository.py`, `trex_core/adapters/persistence/postgresql.py` | 같은 managed-slot lookup 구현 | PostgreSQL은 instance predicate를 반드시 SQL에 넣고, InMemory와 동일한 representative-slot 의미를 제공한다. |
| `docs/runtime-flows.md`, `docs/interface-contract.md`, `docs/operations.md`, 필요 시 `docs/project-structure.md` | REQ-122 baseline 갱신 대상 | REQ-119~121 implementation/QC evidence가 확정되기 전에는 갱신하지 않는다. |
## Key Decisions
### 3. 공통 provider 계약 (REQ-119)
### D1. source mode와 안전한 식별자
신규 application contract는 다음과 같이 제안한다. 실제 파일 위치는 implementation에서 import-cycle 검증 후 위 표의 신규 module 중 하나로 고정한다.
```python
@dataclass(frozen=True)
class BrokerSource:
mode: Literal["env", "fetched", "break_glass"]
identity_label: str # stable, non-secret, masked/fingerprinted only
class BrokerProvider(Protocol):
def get_broker_for_read(self) -> tuple[BrokerPort, BrokerSource]:
"""Return a broker whose credential source was resolved successfully.
Raise BrokerCredentialUnavailable on fetched resolution failure; never
return an env broker as an implicit replacement for fetched mode.
"""
...
def refresh_day_start_credential(self) -> BrokerSource:
"""Resolve/reload the day-start source for daemon ownership.
Raise BrokerCredentialUnavailable on a required fetched-source failure.
"""
...
```
`BrokerSource.identity_label`은 credential version과 one-way masked fingerprint 등 비밀이 아닌 stable value만 허용한다. raw `account_ref`, app key, app secret, access token, Authorization header, raw credential JSON은 provider return value, exception, log, CLI JSON, fixture에 넣지 않는다. label 산출은 단일 helper로 소유하고 fixture의 고정 synthetic label도 같은 형식을 사용한다.
### D2. source resolution 순서와 failure
`credential_fetch_enabled`는 KIS credential source 정책이며 Paper broker의 의미를 바꾸지 않는다. provider는 먼저 `config.broker`를 분기한다.
- `config.broker != "kis"`이면 기존 `PaperBroker`를 구성해 `mode="env"`의 safe source로 반환한다. flag가 `True`여도 portfolio-hub credential endpoint를 호출하거나 KIS로 재구성하지 않으며, 이 조합은 existing backward-compatible configuration으로 유지한다.
- `config.broker == "kis" and credential_fetch_enabled=False`이면 provider는 기존 env/TOML KIS construction을 사용하고 portfolio-hub credential endpoint를 호출하지 않는다. 이는 backward-compatible mode이다.
- `config.broker == "kis" and credential_fetch_enabled=True`이면 다음 순서만 허용한다.
1. 명시적 `TREX_KIS_BROKER_BREAK_GLASS`가 활성화되면 env/TOML source를 선택하고 `mode="break_glass"`로 표시한다. fetch를 시도하지 않으며 activation fact만 secret-safe하게 관찰 가능해야 한다.
2. break-glass가 아니면 portfolio-hub `fetch_broker_credential()`을 호출해 `TradingContext.broker`와 `trading_enabled`를 검증한다.
3. fetch unavailable, malformed/absent credential, `trading_enabled=False`, provider/KIS 재구성 실패는 `BrokerCredentialUnavailable(code=...)`으로 종료한다. env broker를 만들거나 기존 env credential을 계속 사용하지 않는다.
4. 성공한 fetched payload로 KIS broker를 최초 생성하거나 기존 process broker를 `reinitialize()`한다. return source는 `mode="fetched"`이다.
따라서 daemon과 CLI는 source mode가 아니라 provider API를 호출한다. daemon의 day-start scheduler는 `refresh_day_start_credential()`을 호출하고, CLI는 command마다 `get_broker_for_read()`을 호출한다. 같은 invocation에서 `sync`가 여러 slot을 순회하거나 `verify`가 여러 warning을 순회할 때는 provider 반환 broker 하나를 재사용한다.
### D3. lifecycle/reuse 및 retry 결정
- **Ownership:** daemon은 하나의 process-lifetime provider와 그 provider가 소유하는 broker 하나를 갖는다. CLI는 command invocation당 provider 하나와 broker 하나를 갖는다. caller는 broker를 재생성하지 않는다.
- **Reuse scope:** KISBroker의 기존 L1 token과 existing token-cache L2(설정된 경우)를 그대로 reuse한다. credential version/source fingerprint 변경 또는 명시적 fetched refresh 성공 시에만 `reinitialize()`로 L1을 invalidate하고 새 `(account_ref, app_key)` cache key를 사용한다.
- **근거:** 현 코드의 `KISBroker._ensure_token()`은 L1→L2→issuance 경로와 credential-keyed L2를 이미 제공한다. BAT-057은 process-local cache나 새 shared cache를 선결정하지 않고, provider가 fresh broker/object를 반복 만드는 우회 경로를 제거한다.
- **Retry:** token issuance/auth authorization/KIS business failure/malformed response는 즉시 sanitized failure이며 동일 read 안에서 retry하지 않는다. transport timeout/connection failure의 retry 횟수·backoff는 existing HTTP client retry 정책이 없으므로 implementation에서 새 implicit retry를 넣지 않는다(0회). 상위 command가 재시도하려면 별도 invocation/explicit operator action이어야 한다. credential fetch도 이 batch에서는 one attempt fail-loud이다.
- **Invalidation:** source credential 성공 refresh만 credential/token invalidation trigger이다. token cache backend miss/error는 token issuance를 시도할 수 있으나, issuance 실패를 empty balance로 변환하지 않는다.
## Edge Cases
### 4. KIS balance 실패 계약과 no-write barrier (REQ-120)
### D4. typed sanitized failure
`KISBroker.get_balance()`의 반환 계약을 성공 list와 실패가 구별되게 변경한다. 권장 signature는 breaking change를 명시적으로 수행한다.
```python
class BrokerBalanceError(RuntimeError):
def __init__(self, code: Literal[
"token_issuance_failed", "authentication_failed", "authorization_failed",
"transport_failed", "malformed_response", "broker_rejected"
]) -> None: ...
def get_balance(self) -> list[BalanceItem]:
"""Return holdings only after a validated successful KIS balance response.
Raise BrokerBalanceError(code) for any token/auth/transport/shape/KIS failure.
Return [] only for a validated successful response with no positive holdings.
"""
```
`BrokerPort`의 protocol/documentation 및 모든 fake broker를 이 exception contract에 맞춘다. `httpx.HTTPStatusError`와 token JSON/key errors는 `token_issuance_failed`; balance HTTP 401/403은 authentication/authorization; other network/timeout은 `transport_failed`; JSON, `rt_cd` shape, `output1` type/row shape 오류는 `malformed_response` 또는 `broker_rejected`로 map한다. code는 stable allow-list이며 upstream response text, URL query, header, body를 포함하지 않는다.
### D5. propagation and barrier
`reconcile_core_holdings`, `reconcile_positions`, `sync_slot_to_broker`, `verify_slot_reconcile`은 `broker.get_balance()`를 호출하기 전에 write를 하지 않으며 `BrokerBalanceError`를 swallowing하지 않는다. CLI command boundary만 이를 catch하여 `{ "error": {"code": ..., "message": "broker balance unavailable"} }` 형태(기존 `--json` 형식에 맞춰 조정 가능) 또는 stderr를 내고 exit code `1`을 반환한다. error object에는 source `mode`와 safe label은 포함할 수 있으나 secret은 포함하지 않는다.
이 barrier로 다음은 failure path에서 금지된다.
- `sync_slot_to_broker()`의 `broker_qty_zero`/`skip_reason` 생성, slot transition/update, `clear_warned()` 호출
- `verify_slot_reconcile()`의 `cleared`, `not_warned`를 제외한 broker-derived success 판단 및 `clear_warned()` 호출
- reconciliation의 false-flat A-case error quarantine, B/C warning write/clear, empty-holdings/unmanaged classification
- CLI의 verify success 또는 dry-run broker quantity output
실제 successful empty `[]`만 기존 A-case/broker-zero/normal empty semantics에 도달한다. 이 구분은 `sync --dry-run`에도 동일하며 dry-run이라도 실패를 zero preview로 보여주지 않는다.
## 5. managed non-holding divergence (REQ-121)
### D6. repository parity contract
`SlotRepository`에 다음 read-only query를 추가한다. REQ-121 AC-2의 triage context가 필요하므로 boolean existence API가 아니라 representative slot을 반환한다.
```python
def find_managed_slot_by_symbol(self, instance_key: str, symbol: str) -> Optional[Slot]:
"""Return one deterministic managed slot for exact instance+symbol, or None.
Caller invokes only for unmatched broker holdings. The result is read-only;
it must not imply that the slot is holding, eligible, or remediated.
"""
...
```
동일 instance에 historical duplicate symbol rows가 가능하므로 deterministic parity를 위해 PostgreSQL은 `ORDER BY updated_at DESC NULLS LAST, slot_id ASC LIMIT 1`로 고정하고, InMemory도 `updated_at is None`을 가장 오래된 값으로 취급한 동일 key `(updated_at DESC NULLS LAST, slot_id ASC)`를 적용한다. PostgreSQL query는 `WHERE instance_key = %s AND symbol = %s`만 사용하여 holding/inactive/error/pending/retired rows를 포함하며, InMemory도 same exact predicate를 전수 비교한다. `symbol is None` 또는 empty broker symbol은 caller input validation issue이며, query는 read-only라 schema/migration은 필요 없다. index 추가는 실제 production query-plan evidence가 나오기 전 본 hotfix에서 요구하지 않는다. 모든 candidate를 triage 해야 하는 requirement가 아니므로 one representative slot만 structured context에 사용한다. PostgreSQL과 InMemory가 같은 fixture에서 같은 slot identity를 반환하도록 test한다.
### D7. reconciliation 분기와 decision code
`reconcile_core_holdings()`의 holding-slot loop와 기존 A/B quantity handling은 그대로 유지한다. broker holdings loop에서 `bq > 0 and sym not in matched`일 때만 D6 query를 실행한다.
```python
managed = slot_repo.find_managed_slot_by_symbol(instance_key, sym)
if managed is not None:
decisions[f"broker:{sym}"] = "managed_non_holding_divergence"
_log.warning(
"managed non-holding divergence: slot_id=%s portfolio_id=%s "
"symbol=%s status=%s instance_key=%s",
managed.slot_id, managed.portfolio_id, sym, managed.status.value, instance_key,
)
else:
decisions[f"broker:{sym}"] = "unmanaged_position"
_log.warning("unmanaged position: symbol=%s qty=%d instance_key=%s", sym, bq, instance_key)
```
최종 분기는 다음 4-case matrix를 유지한다.
| Input | Existing holding path | Expected decision | Write effect |
| --- | --- | --- | --- |
| broker holding, matching holding slot and same qty | match | `ok` | 없음 |
| broker holding, matching holding slot and qty mismatch | existing B case | `qty_warning` | 기존 warning write만 가능 |
| broker holding, target instance에 어떤 managed slot도 없음 | unmatched | `unmanaged_position` | 없음(기존 safe warning/log만) |
| broker holding, target instance에 `inactive`/`error` 등 non-holding managed slot 존재 | unmatched + managed query hit | `managed_non_holding_divergence` | 없음 |
`managed_non_holding_divergence`는 new additive telemetry/structured warning code이다. slot state transition, warning 자동 해제/생성, auto trade/liquidation, unmanaged auto-slot creation, `sync` invocation을 하지 않는다. convergence는 release-approved existing `sync` path에만 남긴다. 다른 instance에 같은 symbol/portfolio slot이 있어도 query miss여야 한다.
## 6. CLI와 daemon wiring
1. CLI `main()`은 `--config`가 있으면 `resolve_config_toml()` 뒤 `DaemonConfig.from_config(toml_path, env_file=args.env_file)`를 호출하고, config가 없을 때만 `DaemonConfig.from_env()`를 쓴다. 따라서 `sync`/`verify`는 daemon과 같은 instance key, portfolio-hub credential setting 및 `credential_fetch_enabled`를 받는다.
2. `daemon.Daemon.__init__`은 provider를 한 번 assemble하고 `broker, _source = provider.get_broker_for_read()`로 얻은 broker를 `self.broker`에 보관한다. fetched mode startup에서 fetch가 불가능하면 daemon은 explicit safe degraded/entry-blocked state로 남고 env broker로 진행하지 않는다. 기존 scheduler의 `entry_allowed` alarm model은 유지하되 provider failure code를 secret-safe reason으로 전달한다.
3. `SnapshotScheduler`에는 raw `kis_reinit_callback` 대신 `credential_refresh: Callable[[], BrokerSource]` 또는 provider 자체를 dependency로 주입한다. fetched refresh success는 same long-lived broker reinitialize; failure는 `_entry_allowed=False` 및 safe alarm이며 silent env continuation이 아니다. Paper provider는 refresh에서 no-op safe source를 반환하며 fetch/reinitialize를 시도하지 않는다.
4. `_cmd_sync`와 `_cmd_verify`는 `build_broker_provider(cfg).get_broker_for_read()`를 command 초기에 한 번 호출한다. provider acquisition failure면 DB slot target enumeration, cost-basis fetch, warning mutation 이전에 return `1`한다.
5. `sync`의 live double-ack remains before any live write. BAT-057은 flag syntax, CLI command name, wire API를 추가/변경하지 않는다. `verify`는 read-centric라는 기존 boundary를 유지하되 failed read를 success/degrade로 바꾸지 않는다.
6. CLI structured output must use codes/source mode/safe label only. 정확한 output field name은 implementation test와 `--help` evidence 후 REQ-122 baseline에 기록한다.
## 7. 테스트 seam과 검증 설계
| Seam | Double/fixture | Required assertions |
| --- | --- | --- |
| `BrokerProvider` | fake portfolio read with fetched credential, absent credential, fetch exception, break-glass, env-only KIS, Paper | daemon-equivalent/`sync --dry-run`/`verify` source label 및 holding set parity; no implicit fallback; env-only KIS and Paper do not call fetch; break-glass is distinguishable; enabled+Paper keeps PaperBroker. |
| CLI config loading | temp instance TOML with `credential_fetch_enabled=true`, config-dir `.env`, and monkeypatched provider factory | `--config` CLI config equals daemon `from_config()` result; `sync`/`verify` take fetched path when KIS enabled, while no-config stays existing env-only path. |
| KIS adapter | monkeypatched `httpx.post/get` for token, 401/403, timeout, invalid JSON, invalid `rt_cd`/`output1`, successful `[]` | each failure raises only allow-listed `BrokerBalanceError.code`; successful empty returns `[]`; token/header/account/payload never appears in exception/log/JSON. |
| CLI command boundary | provider factory seam injected/monkeypatched; fake repos/warn store recording writes | each failure returns nonzero; no `broker_qty_zero`, slot update/transition, warning clear/write, verify cleared/success; `sync --dry-run` also fails loud. |
| lifecycle | counted KIS/token endpoint + provider call sequence + `InMemoryTokenCache` | one broker per command/daemon lifecycle, L1/L2 reuse prevents fresh issuance; source refresh invalidates/rekeys as intended; throttle/transport still returns sanitized failure. |
| reconciliation | fake broker yielding balance or raising; `InMemorySlotRepository` and recording `SlotService`/warn store | 4-case table exactly; existing match/mismatch unchanged; divergence only logs/decision, no state/warning/trade/remediation writes. |
| repository parity | same fixtures against InMemory and PostgreSQL integration DB | same found/not-found and representative-slot identity; `inactive`/`error` hits; no slot and other-instance-only miss; query performs no write. |
| redaction | caplog/capsys/CLI JSON and source fixtures | deny-list scan/assertion for app key, app secret, token, `Authorization`, raw account number and raw credential payload. |
PostgreSQL parity test may use the repository-local test database fixture only; it must not use production DSN, live broker, or credentials. If local PostgreSQL fixture is unavailable, implementation cannot claim REQ-121 AC-5 complete and must record the environment blocker in verification.
## 8. invariants, compatibility, non-goals, rollback
### Invariants
- fetched mode without explicit break-glass: fetch failure means no broker truth and no env/config fallback.
- any broker balance failure: nonzero operator outcome and no broker-derived state/warning/slot write or false-zero classification.
- actual successful empty holdings remain semantically distinct and preserve existing behavior.
- `config.broker != "kis"` (Paper 포함)은 `credential_fetch_enabled` 값과 무관하게 existing broker를 유지하고 portfolio-hub credential fetch/KIS reinitialize를 하지 않는다.
- `config.broker == "kis" and credential_fetch_enabled=False`는 env-only로, portfolio-hub credential request와 CLI syntax regression 없이 유지된다.
- CLI `--config`는 daemon과 같은 `DaemonConfig.from_config()` path를 사용하므로 instance-level fetched-mode policy를 잃지 않는다.
- managed non-holding is read-first additive triage only; all persistence mutations remain on pre-existing validated flows.
- every repository managed-slot lookup is instance-scoped; no cross-instance ownership inference.
- external portfolio-hub wire contract, DB schema, AMQP payload, slot state enum, automatic remediation and live sync authorization do not change.
### Non-goals
- portfolio-hub credential storage/API redesign, token cache infrastructure replacement, global retry framework, bulk/global sync, automatic slot creation, auto trade/liquidation/clear, TVR strategy changes, TVR redeploy, production read/live sync execution.
### Rollback
- The code change must be independently revertible as a single hotfix commit/PR. Revert restores prior construction and decision behavior; it does not mutate existing slots or warnings because the new provider/failure and divergence branches are read-first.
- If fetched provider rollout fails, operator remediation is to stop broker-read/sync, retain evidence with safe codes, and use the separately approved break-glass policy only when an operator explicitly enables it. Do not silently toggle configuration or run `sync --live` as rollback.
- Baseline documentation update is rolled back with the implementation change if implementation evidence is rejected; REQ-122 must not describe unlanded source mode/code/command behavior.
## Architecture Impact
- **both**: `runtime-flows`는 daemon/CLI 공통 provider resolution, KIS balance fail-loud hold 및 두 divergence decision 흐름을 반영해야 한다. `project-structure`는 provider/factory가 stable module로 landed한 경우에만 새 ownership boundary를 반영한다.
- `interface-contract.md`와 `operations.md`도 REQ-122 범위에서 갱신 대상이나, portfolio-hub/AMQP 외부 wire 계약, DB schema 및 live-sync authorization은 변경하지 않는다.
- 실제 baseline 수정은 구현과 독립 QC evidence가 확정된 뒤에만 수행한다.
## 9. REQ acceptance traceability
| REQ / AC | Design element | Required verification evidence |
| --- | --- | --- |
| REQ-119 AC-1 | D1-D3 common provider; §6 same assembly | mocked fetched credentials; daemon-equivalent, `sync --dry-run`, `verify` same safe source label and holding set |
| REQ-119 AC-2 | D2 failure order; D5 barrier | fetch failure/no break-glass → safe error, nonzero, no env broker/qty inference |
| REQ-119 AC-3 | D1-D2 break-glass `mode` and safe observability | explicit break-glass fixture shows distinct safe mode/activation basis |
| REQ-119 AC-4 | D2의 env-only KIS/Paper branches 및 §6 CLI config loading | env-only KIS와 enabled+Paper fetch call count zero, existing Paper regression, `--config` CLI/daemon instance-policy parity |
| REQ-119 AC-5 | D1, §7 redaction seam | logs/JSON/fixtures deny-list assertions |
| REQ-120 AC-1 | D4-D5 strict `get_balance` and CLI boundary | token/auth/transport/malformed fixtures yield distinct sanitized failure and nonzero CLI |
| REQ-120 AC-2 | D5 no-write barrier | recording repo/warn store proves no zero skip, success, clear or slot write for every failure class |
| REQ-120 AC-3 | D4 successful-empty contract | valid empty `output1` gives `[]` and preserves normal empty path |
| REQ-120 AC-4 | D3 lifecycle ownership/reuse | repeated fetched reads counted for broker/token construction; throttle error remains fail-loud |
| REQ-120 AC-5 | D3 policy | implementation/design test records ownership, reuse/invalidation and 0-retry policy without requiring cache mechanism replacement |
| REQ-120 AC-6 | D1/D4/D5 and §7 redaction | failure logs/JSON/fixtures contain only safe code/mode/label |
| REQ-121 AC-1 | D7 unmatched miss | no managed slot → `unmanaged_position`, existing safe warning semantics |
| REQ-121 AC-2 | D6 richer repository lookup + D7 | inactive/error same-instance slot → `managed_non_holding_divergence` with required safe triage context |
| REQ-121 AC-3 | D7 4-case matrix | match and qty mismatch remain `ok`/`qty_warning`, no reclassification |
| REQ-121 AC-4 | D7 invariants and recording persistence seam | divergence creates no slot/warning state write, clear, trade, liquidation or auto-slot |
| REQ-121 AC-5 | D6 repository parity | PostgreSQL/InMemory same found/miss and selected representative result |
| REQ-121 AC-6 | D6 exact instance predicate | other-instance-only matching symbol/portfolio is a miss |
| REQ-122 AC-1 | §10 runtime baseline plan | post-implementation runtime flow matches provider, strict failure, two divergence codes/no-auto-remediation |
| REQ-122 AC-2 | §10 interface baseline plan | actual CLI exit/output safe fields and repository/reconciliation contract; wire-change statement |
| REQ-122 AC-3 | §10 operations baseline plan | dry-run → cross-check → explicit approval → one target live sync → re-reconcile sequence, with no target/command authorization claim |
| REQ-122 AC-4 | §7 redaction + §10 doc secret scan | baseline examples/log/JSON samples pass deny-list review |
| REQ-122 AC-5 | §10 evidence-before-doc rule | CLI help, implemented decision codes/tests, references cross-checked; unresolved facts not documented as policy |
## Reference Doc Update Plan (identify only; do not update in this gate)
REQ-119~121 implementation and independent QC evidence가 확정된 뒤에만 REQ-122 delivery에서 다음을 갱신한다.
- `docs/runtime-flows.md`: fetched/env/break-glass provider resolution, daemon/CLI parity, balance failure hold, successful-empty distinction, `unmanaged_position`/`managed_non_holding_divergence` flow와 no-auto-remediation 경계.
- `docs/interface-contract.md`: actual `sync`/`verify` broker-read failure exit semantics, safe output fields, `BrokerBalanceError`/repository lookup의 internal contract, AMQP/portfolio-hub external wire contract unchanged 여부.
- `docs/operations.md`: masked source/account reference evidence, failure hold, dry-run → broker/ledger/slot cross-check → 사장님 explicit approval → single-target `sync --live --yes` → re-reconcile release gate. 이 문서는 live target을 선택하거나 실행을 승인하지 않는다.
- `docs/project-structure.md`: provider/factory가 신규 stable module로 landed한 경우에만 dependency/ownership 경계를 반영한다.
- `docs/srs/index.md` 및 REQ-119~122 status/Change Log: merge/main and verification evidence 이후 StagePilot 절차에 따라 갱신한다. 본 design gate에서 상태를 변경하지 않는다.
- REQ-112, REQ-113, REQ-115, REQ-103, REQ-105 references: implemented source priority, day-start lifecycle, reconcile decision terminology와 충돌 여부를 evidence 기반으로 cross-check한다.
## 11. Design-gate exit conditions
- [ ] provider source/failure/reuse contract과 break-glass boundary가 implementation owner에게 명확하다.
- [ ] strict balance exception mapping 및 CLI no-write barrier test seam이 확정되었다.
- [ ] repository lookup의 deterministic rich return, PostgreSQL/InMemory parity, instance isolation rule이 확정되었다.
- [ ] 4-case reconciliation matrix가 existing A/B/C behavior와 additive divergence effect를 구분한다.
- [ ] REQ-119~122 AC별 test/document evidence가 §9에 연결되었다.
- [ ] live service, credentials, production DB, `sync --live`를 사용하지 않았고 implementation/baseline update authorization과 혼동하지 않았다.
@@ -0,0 +1,20 @@
# BAT-057 Implementation
- Status: draft
- Batch: BAT-057
## Plan Summary
- Implementation stage에서 REQ-119~121의 broker source parity, KIS fail-loud/no-write 및 managed non-holding divergence 분류를 feature worktree/PR flow로 구현하고, REQ-122는 검증 evidence 이후 문서화한다.
## Changed Files
- To be determined at design gate; no implementation authorized in this planning handoff.
## Execution Log
1. Not started. Batch planning only.
## Validation
- Not started. Independent QC must re-run the implementation evidence before verification confirmation.
## Remaining Risks
- Design gate 미완료 상태에서 provider/lifecycle/security 계약을 추정하지 않는다.
- `sync --live` 및 production command는 이 batch 단계에서 실행하지 않는다.
@@ -0,0 +1,33 @@
# BAT-057: operator broker truth 및 managed divergence emergency hotfix
- Status: in-delivery
- Profile: standard
- Source Discovery: dcy-037_20260714_operator-broker-truth-and-managed-slot-divergence-hotfix
- Included REQ: REQ-119, REQ-120, REQ-121, REQ-122
- Excluded REQ: 없음
## Summary
- dcy-037에서 승인한 #39/#40/#41을 하나의 emergency delivery 단위로 묶어 fetched credential broker truth 정합, auth/token 실패 fail-loud, managed non-holding divergence 분류 및 baseline 문서 정합을 함께 계획한다.
- `sync --live` 실행과 production target 선택은 이 batch에서 승인하거나 수행하지 않고 별도 release approval gate로 유지한다.
## Scope
- In Scope:
- REQ-119: daemon/operator broker-read 공통 fetched credential provider와 no-silent-fallback/secret-safe 관찰성
- REQ-120: KIS balance auth/token/transport/malformed failure와 실제 empty balance의 분리, no-write 및 lifecycle 설계 제약
- REQ-121: instance-scoped managed-slot existence query와 `unmanaged_position`/`managed_non_holding_divergence` additive 분류
- REQ-122: 구현·검증 evidence 이후 runtime/interface/operations baseline 정합 및 remediation release gate 문서화
- Out of Scope:
- portfolio-hub credential API·저장 모델 재설계
- `sync --live` 자동 실행, 전역/bulk sync, unmanaged position 자동 slot 생성 또는 자동 remediation/trading
- TVR 전략 로직 변경 및 TVR 재배포
## Documents
- Planning: [planning.md](./planning.md)
- Design: [design.md](./design.md)
- Implementation: [implementation.md](./implementation.md)
- Verification: [verification.md](./verification.md)
## Notes
- Cross-service contract, security, migration 또는 scope trade-off가 발견되면 구현을 진행하지 않고 lead gate로 중단한다.
- 모든 evidence는 credential, token, app key/secret, raw account number/payload를 포함하지 않는 masked label/fingerprint만 사용한다.
- 다음 gate는 design이며, implementation은 feature worktree/PR와 independent QC를 거쳐야 한다.
@@ -0,0 +1,45 @@
# BAT-057 Planning
- Status: confirmed
- Batch: BAT-057
- Profile: standard
## Included REQ
- REQ-119 (Approved): fetched credential 공통 broker provider와 operator CLI truth 정합
- REQ-120 (Approved): KIS balance 인증 실패 fail-loud 및 broker/token lifecycle 정책
- REQ-121 (Approved): managed non-holding divergence 분류와 slot 존재 조회 계약
- REQ-122 (Approved): operator broker truth HOTFIX baseline 및 remediation 문서 정합
## Delivery Plan
1. 공통 broker provider/factory와 daemon·`sync`·`verify`의 fetched credential source parity 경계를 설계한다. fetched mode fetch 실패 시 silent env/config fallback을 금지하고, env-only mode 회귀를 보존한다.
2. KIS balance failure class를 actual empty balance와 분리하고, operator broker-read의 non-zero/no-write semantics 및 token/broker lifecycle ownership·reuse·retry 정책을 evidence 기반으로 확정한다.
3. reconciliation의 unmatched broker symbol에 한해 instance-scoped managed-slot existence query를 추가하고, 기존 holding match/mismatch와 true unmanaged semantics를 보존하면서 additive divergence code를 도입한다.
4. REQ-119~121 구현 및 independent QC evidence가 준비된 후 REQ-122의 runtime/interface/operations baseline을 실제 계약과 대조해 갱신한다.
5. unit/integration/CLI regression과 secret-redaction 검증을 구현 lane 및 독립 QC lane에서 각각 수행한다. production command 또는 `sync --live`는 실행하지 않는다.
## Dependencies
- Internal:
- 기존 REQ-112 credential read port, REQ-113 source priority/break-glass, REQ-115 day-start credential resilience
- `trex_core/runtime/daemon.py`, `trex_core/runtime/cli.py`, `trex_core/adapters/broker/kis.py`, `trex_core/application/reconciliation.py`
- SlotRepository protocol 및 PostgreSQL/InMemory 구현
- 기존 sync/verify gate와 broker/ledger/slot read semantics
- External:
- portfolio-hub fetched credential authority는 기존 계약을 사용한다. 계약 변경이 필요하면 본 batch 범위를 중단하고 cross-service gate로 전환한다.
## Design Gate
- Design Required Before Implementation: yes
- Reason: 공통 provider 경계, KIS token/broker lifecycle ownership·reuse·retry 정책, failure type/no-write semantics, repository query 계약 및 interface/runtime 영향이 구현 전에 확정되어야 한다. 특히 REQ-120 OQ-2는 process-local/shared cache/other provider 중 방식을 근거 없이 선결정할 수 없다.
## Milestones
1. Batch 문서 및 planning 확정: 승인된 REQ-119~122와 dcy-037 scope traceability를 고정한다.
2. Design gate: provider/lifecycle/failure/reconciliation 계약과 cross-service·security 영향 확인.
3. Feature worktree implementation: REQ-119~121 코드·회귀 테스트 구현, REQ-122는 evidence 이후 문서 반영.
4. Independent QC: fetched source parity, fail-loud/no-write, true-empty 분리, 4-case divergence matrix, repository parity, instance isolation, secret scan을 재실행.
5. Release gate: 배포 전 dry-run 및 broker/ledger/slot cross-check 후 lead/사장님 명시 승인 없이는 `sync --live`를 실행하지 않는다.
## Risks
- R1 High: fetched credential source/account identity mismatch가 다른 계좌 truth로 이어질 수 있음. masked source identity parity와 dry-run evidence로 완화한다.
- R2 High: auth/token/transport failure가 false zero로 변환되면 잘못된 state convergence가 발생함. typed/sanitized failure와 no-write assertion을 고정한다.
- R3 Medium: managed-slot existence query가 기존 holding mismatch/unmanaged semantics를 바꿀 수 있음. unmatched symbol에만 적용하고 4-case matrix를 유지한다.
- R4 Medium: 반복 fresh broker/token 생성이 throttle을 유발할 수 있음. lifecycle reuse 범위와 failure-class별 retry/no-retry를 design evidence로 결정한다.
- R5 High: hotfix 후 승인 없는 live sync가 외부 상태를 변경할 수 있음. `sync --live`를 별도 release approval gate로 유지하고 이번 batch에서 실행하지 않는다.
@@ -0,0 +1,24 @@
# BAT-057 Verification
- Status: draft
- Batch: BAT-057
## Acceptance Mapping
- REQ-119: fetched source parity, no-silent-fallback, env-only regression, safe identity/redaction evidence.
- REQ-120: failure-class vs true-empty separation, non-zero/no-write semantics, lifecycle/throttle behavior, secret-safe artifacts.
- REQ-121: four-case reconciliation matrix, PostgreSQL/InMemory parity, instance isolation, no-auto-remediation.
- REQ-122: runtime/interface/operations documentation cross-check against implementation evidence and CLI/help/decision-code reality.
## Evidence
- Test:
- To be collected after implementation; independent QC re-execution required.
- Manual:
- Release-gated dry-run evidence only; no live sync authorized in this batch handoff.
- Logs:
- Masked source labels/fingerprints and sanitized codes only; no credential/token/account disclosure.
## Result
- Ready for Release: false
- Blocking Issues:
- Design, implementation and independent QC are not complete.
- `sync --live` requires separate release approval and is not authorized.
+1
View File
@@ -62,3 +62,4 @@
| BAT-052 | standard | release-candidate | REQ-109, REQ-110, REQ-111 | dcy-035 | [bat-052](./bat-052_20260626_wac-realized-pnl-report/) |
| BAT-053 | standard | release-candidate | REQ-112, REQ-113, REQ-114, REQ-115 | dcy-036 | [bat-053](./bat-053_20260628_daystart-credential-fetch-failsafe/) |
| BAT-054 | standard | in-progress | REQ-113 (AC-2/AC-4 보강), REQ-115 (AC-1) | dcy-036 | [bat-054](./bat-054_20260628_daystart-credential-kis-inject/) |
| BAT-057 | standard | in-delivery | REQ-119, REQ-120, REQ-121, REQ-122 | dcy-037 | [bat-057](./bat-057_20260714_operator-broker-truth-hotfix/) |