<!-- task=m-backtest-data-collection-infrastructure/01_watchlist_config plan=1 tag=REVIEW_CLI -->

# Plan - REVIEW_CLI Watchlist Config Follow-up

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성은 필수다. 구현 후 검증을 실행하고 실제 stdout/stderr를 붙인 뒤 active 파일을 그대로 둔 채 리뷰 준비 상태로 보고한다. 종결, log rename, `complete.log`, archive 이동은 code-review 전용이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 범위 충돌이 필요하면 review stub의 `사용자 리뷰 요청` 섹션에 근거를 기록하고 멈춘다. 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 증거 공백은 사용자 리뷰 요청이 아니다.

## 배경

이 follow-up은 `code_review_local_G05_0.log`의 Required 이슈만 처리한다. named universe 구현 자체는 기본 테스트를 통과했지만, `Scenario.Validate()`가 materialization 뒤 재호출에 취약하고, fixture 기반 runner 테스트가 API request의 universe-derived 필드를 충분히 고정하지 못한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/backtest-loop/milestones/backtest-data-collection-infrastructure.md`
- Task ids:
  - `watchlist-config`: 선택 종목군을 수집 대상으로 등록하고 갱신할 수 있는 headless 설정 경계를 만든다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/private/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-roadmap/current.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-ops/rules/project/domain/operations/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/operations-smoke.md`
- `agent-task/m-backtest-data-collection-infrastructure/01_watchlist_config/plan_local_G05_0.log`
- `agent-task/m-backtest-data-collection-infrastructure/01_watchlist_config/code_review_local_G05_0.log`
- `apps/cli/internal/operator/scenario.go`
- `apps/cli/internal/operator/scenario_test.go`
- `apps/cli/internal/operator/runner.go`
- `apps/cli/internal/operator/runner_market_test.go`
- `apps/cli/internal/operator/client_test.go`
- `apps/cli/testdata/operator/market_data_status_query.yaml`
- `apps/cli/testdata/operator/us_market_data_status_query.yaml`
- `apps/cli/testdata/operator/kis_daily_import_smoke.yaml`

### 테스트 환경 규칙

- `test_env=local`.
- 변경 범위는 `apps/cli/**`이므로 operations 도메인 규칙과 local Go test를 적용한다.
- 적용 명령: `go test ./apps/cli/internal/operator ./apps/cli/internal/cli`.
- 외부 서비스, secret, Docker infra 필요 없음.

### 실패 요약

- `scenario.go`는 universe materialization 후 `Request.Universe`를 그대로 둔다. 이미 materialize된 scenario에 `Validate()`를 다시 호출하면 helper가 자체 채운 inline fields를 사용자 inline conflict로 오인한다.
- `runner_market_test.go`는 loaded fixture에서 import request가 실제 API로 나갔는지 확인하지만 `provider`와 날짜만 보며, named universe가 제공하는 `selector_kind`, `market`, `venue`, `name`, `symbols`는 고정하지 않는다.

### 범위 결정 근거

- protobuf schema, API/worker service, YAML fixture 구조 자체는 변경하지 않는다.
- 이번 follow-up은 validation idempotence와 runner test coverage만 보완한다.
- milestone-linked task group이므로 PASS 시 code-review가 runtime completion metadata를 보고한다.

### 빌드 등급

- `local-G06`: 명확한 Required 이슈 두 개이며 로컬 unit test로 재현/검증 가능하다. 범위는 `apps/cli/internal/operator` 테스트와 validation helper에 제한된다.

## 구현 체크리스트

- [ ] `Scenario.Validate()`의 named universe materialization을 재호출해도 실패하지 않도록 idempotent하게 고친다.
- [ ] loaded KR/US fixture runner 테스트가 materialized import request의 `selector_kind`, `market`, `venue`, `name`, `symbols`까지 API 요청으로 전달되는지 검증하게 보강한다.
- [ ] `go test ./apps/cli/internal/operator ./apps/cli/internal/cli`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_CLI-1] Idempotent Universe Materialization

#### 문제

`apps/cli/internal/operator/scenario.go:467`의 conflict check는 `Request.Universe`가 있는 상태에서 inline import field가 하나라도 채워져 있으면 실패한다. 같은 helper가 `apps/cli/internal/operator/scenario.go:477`부터 provider/selector/market/venue/name/symbols를 채우지만 `Request.Universe`를 비우지 않으므로, `ParseScenario` 뒤 `sc.Validate()`를 재호출하면 실제 사용자 inline conflict가 없는데도 실패한다.

#### 해결 방법

성공적으로 universe를 materialize한 뒤 `step.Request.Universe = ""`로 정리해 validated scenario가 기존 inline import request와 같은 상태가 되게 한다. user-authored conflict는 materialization 전 검사에서 계속 실패해야 한다.

```go
step.Request.Provider = u.Provider
step.Request.SelectorKind = u.SelectorKind
step.Request.Market = u.Market
step.Request.Venue = u.Venue
step.Request.Name = u.Name
step.Request.Symbols = append([]string(nil), u.Symbols...)
step.Request.Universe = ""
```

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

- [ ] `apps/cli/internal/operator/scenario.go`: successful materialization 후 `Request.Universe`를 clear한다.
- [ ] `apps/cli/internal/operator/scenario_test.go`: universe-backed scenario를 `ParseScenario`한 뒤 `sc.Validate()`를 다시 호출해도 성공하는 regression test를 추가한다.
- [ ] 기존 `TestValidateRejectsUniverseInlineConflict`가 계속 실패하는지 유지한다.

#### 테스트 작성

- `TestValidateImportDailyBarsUniverseReferenceIsIdempotent`: `ParseScenario` 성공 후 `sc.Validate()` 재호출이 nil을 반환하는지 확인한다.

#### 중간 검증

```bash
go test ./apps/cli/internal/operator
```

기대 결과: exit code 0.

### [REVIEW_CLI-2] Runner Fixture Request Assertions

#### 문제

`apps/cli/internal/operator/runner_market_test.go:143`은 `market_data_status_query.yaml` fixture를 load한 뒤 fake API가 받은 import request를 확인하지만 `provider`와 날짜만 assert한다. named universe는 `selector_kind`, `market`, `venue`, `name`, `symbols`도 materialize하므로 이 값들이 runner/API request 경계까지 보존되는지 테스트가 잠가야 한다.

#### 해결 방법

KR fixture runner test를 확장하고, US fixture runner test도 import request assertion을 추가한다. fake API가 이미 `lastImportDailyBarsReq()`를 제공하므로 새 test infrastructure는 필요 없다.

```go
if imp.GetSelectorKind() != "watchlist" { ... }
if imp.GetMarket() != altv1.Market_MARKET_KR { ... }
if imp.GetVenue() != altv1.Venue_VENUE_KRX { ... }
if imp.GetName() != "kr-core" { ... }
if !equalStrings(imp.GetSymbols(), []string{"005930"}) { ... }
```

US fixture는 `MARKET_US`, `VENUE_NASDAQ`, `name=us-core`, `symbols=[AAPL]`을 확인한다.

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

- [ ] `apps/cli/internal/operator/runner_market_test.go`: KR loaded fixture import request assertion을 `selector_kind`, `market`, `venue`, `name`, `symbols`까지 확장한다.
- [ ] `apps/cli/internal/operator/runner_market_test.go`: US loaded fixture import request assertion을 추가한다.

#### 테스트 작성

- 기존 `TestRunScenarioMarketDataStatusImportsBeforeReads` 확장.
- 기존 `TestRunScenarioMarketDataStatusUS` 또는 새 US-specific test에서 API request fields 확인.

#### 중간 검증

```bash
go test ./apps/cli/internal/operator
```

기대 결과: exit code 0.

## 최종 검증

```bash
go test ./apps/cli/internal/operator ./apps/cli/internal/cli
```

기대 결과: exit code 0. Go test cache output은 허용한다. 모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다.
