<!-- task=m-live-trading-boundary/05+02,03,04_audit_trail plan=1 tag=REVIEW_LIVE_AUDIT -->

# Plan - REVIEW_LIVE_AUDIT

## 이 파일을 읽는 구현 에이전트에게

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것은 필수다. 구현 후 검증을 실행하고 실제 변경 내용, 설계 결정, 명령 출력(stdout/stderr)을 기록한 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 최종 판정, 로그 rename, `complete.log`, archive 이동은 code-review 전용이다.

구현 중 사용자만 결정할 수 있는 사항, 사용자 소유 외부 환경/secret, 또는 계획 범위 충돌 없이는 안전하게 진행할 수 없으면 active review stub의 `사용자 리뷰 요청` 섹션에 정확한 결정, 근거, 실행 명령/출력, 재개 조건을 기록하고 중단한다. 구현 에이전트는 사용자에게 직접 묻거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백은 사용자 리뷰 요청이 아니다.

## 배경

1차 리뷰(`code_review_cloud_G07_0.log`)는 live audit trail 구현을 `FAIL`로 판정했다. 생성물 check와 Go/Flutter focused 검증은 통과했지만, 실제 worker process가 `Deps.Live`를 배선하지 않아 API/CLI audit query가 runtime에서 unavailable로 떨어지고, 성공 주문 audit event와 raw payload rejection 요구가 계획 대비 누락됐다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/trading-expansion/milestones/live-trading-boundary.md`
- Task ids:
  - `audit-trail`: live operation audit trail이 있다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-task/m-live-trading-boundary/05+02,03,04_audit_trail/plan_cloud_G07_0.log`
- `agent-task/m-live-trading-boundary/05+02,03,04_audit_trail/code_review_cloud_G07_0.log`
- `services/worker/cmd/alt-worker/main.go`
- `services/worker/cmd/alt-worker/main_test.go`
- `services/worker/internal/socket/live.go`
- `services/worker/internal/socket/handlers.go`
- `services/worker/internal/livetrading/service.go`
- `services/worker/internal/livetrading/service_test.go`
- `services/worker/internal/storage/postgres/store.go`
- `services/worker/internal/storage/postgres/mapping.go`
- `services/worker/internal/storage/postgres/mapping_test.go`

### 테스트 환경 규칙

`test_env=local`이다. 이번 follow-up은 worker runtime wiring, livetrading service event emission, postgres audit storage append validation을 고친다. 필수 검증은 focused worker tests, `bin/worker-storage-check`, 그리고 최종 worker/API/CLI smoke confidence다.

### 범위 결정 근거

이 follow-up은 리뷰 Required만 고친다. KIS 실주문 BrokerPort 구현, Flutter 화면, audit 상세 CLI row rendering 확장은 범위 밖이다. 현재 KIS `LiveProvider`가 `livetrading.BrokerPort`를 구현하지 않으면, worker bootstrap은 `livetrading.NewService(nil)`로 live service를 만들고 audit store만 주입해 query surface를 열어도 된다. 이 경우 order/sync broker operations는 기존처럼 typed unavailable을 반환하되 `ListLiveAuditEvents`는 durable store를 조회해야 한다.

### 빌드 등급

Build lane `cloud-G07`, review lane `cloud-G07`. Worker bootstrap/runtime contract, audit persistence safety, operation event semantics가 함께 걸려 이전 cloud route를 유지한다.

## 구현 체크리스트

- [ ] [REVIEW_LIVE_AUDIT-1] worker bootstrap에 live service와 audit store를 배선한다.
- [ ] [REVIEW_LIVE_AUDIT-2] 성공 submit audit event를 `submit_confirmed`와 `broker_accepted`/`broker_rejected`로 분리한다.
- [ ] [REVIEW_LIVE_AUDIT-3] forbidden audit payload key를 append 경계에서 거부하고 테스트를 보강한다.
- [ ] [REVIEW_LIVE_AUDIT-4] focused verification과 최종 검증을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_LIVE_AUDIT-1] Worker Runtime Wiring

#### 문제

`services/worker/cmd/alt-worker/main.go:96`의 `storeBackedDeps`가 `socket.Deps.Live`를 채우지 않는다. `services/worker/internal/socket/live.go:397`은 `deps.Live == nil`이면 `ListLiveAuditEvents`를 `unavailable`로 반환하므로 API/CLI audit query workflow가 실제 worker process에서 동작하지 않는다.

#### 해결 방법

`storeBackedDeps`에서 live service를 생성하고, 같은 postgres store를 audit store로 주입한다.

예시:

```go
liveService := livetrading.NewService(nil)
liveService.SetAuditStore(store)
```

KIS broker port가 현재 `livetrading.BrokerPort`를 구현하지 않으면 nil broker를 유지한다. 이 follow-up은 broker adapter 구현이 아니라 durable audit query surface 배선이 목표다.

#### 수정 파일 및 체크리스트

- [ ] `services/worker/cmd/alt-worker/main.go`에 `livetrading` import와 `Live: liveService` 배선을 추가한다.
- [ ] `services/worker/cmd/alt-worker/main_test.go`가 `deps.Live != nil`을 검증한다.
- [ ] 같은 테스트에서 `deps.Live.ListLiveAuditEvents(context.Background(), trading.AuditFilter{AccountID: "acct"})` 호출이 nil deps unavailable가 아니라 store-backed path를 타는지 검증한다. nil pool store가 실제 DB 호출까지 가면 안 되므로, 가능한 범위에서는 fake/narrow store 또는 no-op assertion으로 배선만 확인한다.

#### 테스트 작성

작성한다. 최소한 `storeBackedDeps`가 live service를 반환하고 hello capability에 `live-trading`이 포함될 수 있는 조건을 보장한다.

#### 중간 검증

```bash
go test ./services/worker/cmd/alt-worker ./services/worker/internal/socket
```

기대 결과: exit code 0.

### [REVIEW_LIVE_AUDIT-2] Accepted Broker Audit Event

#### 문제

`services/worker/internal/livetrading/service.go:343`은 성공 submit 결과를 `submit_confirmed` 하나로만 기록하고, rejected일 때만 `broker_rejected`로 바꾼다. `broker_accepted` constant는 정의되어 있지만 사용되지 않는다. 계획은 operator confirmation과 broker response를 분리된 audit event로 남기는 것이다.

#### 해결 방법

`SubmitLiveOrder` 성공 경로에서 audit append를 두 단계로 분리한다.

- operator confirmation event: `submit_confirmed`
- broker response event: `broker_accepted` when `order.Status != trading.OrderStatusRejected`, otherwise `broker_rejected`

두 event 모두 raw token/account/provider payload 없이 sanitized `map[string]string`만 담는다. 기존 mandatory audit policy를 유지해 append failure는 `ErrAuditFailed`로 반환한다.

#### 수정 파일 및 체크리스트

- [ ] `services/worker/internal/livetrading/service.go`에서 성공 submit audit event를 두 개로 분리한다.
- [ ] `services/worker/internal/livetrading/service_test.go`의 success test가 `submit_confirmed`와 `broker_accepted`를 모두 확인하게 보강한다.
- [ ] broker rejected result test가 없으면 추가해 `broker_rejected` event를 확인한다.

#### 테스트 작성

작성한다. 성공 submit에서 event type 순서와 `order_id`, `actor`, `broker_order_id` payload가 기대대로 들어가는지 확인한다.

#### 중간 검증

```bash
go test ./services/worker/internal/livetrading -run 'TestSubmitLiveOrder.*Audit|TestCancelLiveOrderEmitsCancelConfirmedAudit|TestSyncLiveAccount.*Audit|TestListLiveAuditEvents' -count=1 -v
```

기대 결과: exit code 0.

### [REVIEW_LIVE_AUDIT-3] Audit Payload Rejection

#### 문제

`services/worker/internal/storage/postgres/store.go:220`과 `services/worker/internal/storage/postgres/mapping.go:335`는 payload를 그대로 JSONB로 저장한다. `mapping_test.go:405`의 테스트 이름은 raw secret rejection처럼 보이지만 실제로는 `"token"` payload가 marshal되는지만 확인한다.

#### 해결 방법

append 경계에서 forbidden payload key를 거부한다. storage-level `AppendLiveAuditEvent` 또는 `mapAuditEventToParams` 중 한 곳에 `trading.HasRawSecretKey(event.Payload)` 검사를 두고, error message는 테스트 가능한 문자열을 포함한다. 필요하면 domain blocked key 목록을 `account_number`, `raw_provider_body`, `raw_provider_response`, `provider_response`처럼 계획에서 금지한 provider/account raw payload 표현까지 확장한다.

#### 수정 파일 및 체크리스트

- [ ] `packages/domain/trading/audit.go`의 forbidden key 목록이 계획의 raw token/account/provider-body 금지 범위를 커버하는지 보강한다.
- [ ] `services/worker/internal/storage/postgres/store.go` 또는 `mapping.go`에서 forbidden key append를 error로 막는다.
- [ ] `services/worker/internal/storage/postgres/mapping_test.go`의 raw secret 테스트를 실제 rejection assertion으로 바꾼다.
- [ ] 필요하면 `packages/domain/trading/audit_test.go`에 새 forbidden key case를 추가한다.

#### 테스트 작성

작성한다. `token`, `raw_account_number`, provider raw body/response 계열 key가 append/mapping에서 error를 반환해야 한다. safe payload는 기존 round trip이 유지되어야 한다.

#### 중간 검증

```bash
go test ./packages/domain/... ./services/worker/internal/storage/postgres -run 'TestHasRawSecretKey|TestAuditEventMapping|TestStoreLiveAudit' -count=1 -v
```

기대 결과: exit code 0.

### [REVIEW_LIVE_AUDIT-4] Verification

#### 문제

1차 검증 명령은 모두 통과했지만 Required 동작을 잡는 테스트가 없었다. Follow-up은 focused failure tests를 추가한 뒤 전체 affected smoke를 다시 실행해야 한다.

#### 해결 방법

리뷰 Required를 재현하는 테스트가 먼저 실패하는 구조로 작성하고, 구현 후 전체 affected 명령을 실행한다.

#### 수정 파일 및 체크리스트

- [ ] `gofmt -l`가 빈 출력인지 확인한다.
- [ ] `bin/worker-storage-check`를 실행한다.
- [ ] `bin/contracts-check`를 실행한다.
- [ ] `go test ./packages/domain/... ./packages/contracts/gen/go/...`를 실행한다.
- [ ] `go test ./services/worker/...`를 실행한다.
- [ ] `go test ./services/api/...`를 실행한다.
- [ ] `go test ./apps/cli/...`를 실행한다.
- [ ] `cd apps/client && flutter test test/contracts/alt_contracts_test.dart`를 실행한다.
- [ ] `bin/lint`를 실행한다.
- [ ] `git diff --check`를 실행한다.

#### 테스트 작성

REVIEW_LIVE_AUDIT-1부터 REVIEW_LIVE_AUDIT-3에 포함한다.

#### 중간 검증

```bash
gofmt -l services/worker/cmd/alt-worker/main.go services/worker/cmd/alt-worker/main_test.go services/worker/internal/livetrading/service.go services/worker/internal/livetrading/service_test.go services/worker/internal/storage/postgres/store.go services/worker/internal/storage/postgres/mapping.go services/worker/internal/storage/postgres/mapping_test.go packages/domain/trading/audit.go packages/domain/trading/audit_test.go
bin/worker-storage-check
bin/contracts-check
go test ./packages/domain/... ./packages/contracts/gen/go/...
go test ./services/worker/...
go test ./services/api/...
go test ./apps/cli/...
cd apps/client && flutter test test/contracts/alt_contracts_test.dart
bin/lint
git diff --check
```

기대 결과: 모든 명령 exit code 0. `bin/lint`가 기존 Mattermost `avoid_print` info만 출력하면 기존 이슈로 기록한다.
