<!-- task=m-backtest-data-collection-infrastructure/01_input_selector_contract plan=0 tag=API -->
# PLAN-cloud-G06: Backtest Input Selector Contract

## 이 파일을 읽는 구현 에이전트에게
이 계획은 backtest run input에 종목군 selector를 싣기 위한 계약, domain, mapping, round-trip 테스트만 만든다. 구현 후 반드시 검증을 실행하고 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션에 실제 변경 내용과 stdout/stderr를 채운 뒤 active 파일을 유지한 채 리뷰 준비를 보고한다. 최종 PASS, log rename, `complete.log`, archive 이동은 code-review skill 전용이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret, 또는 범위 충돌 때문에 멈춰야 하면 사용자에게 직접 묻지 말고 review stub의 `사용자 리뷰 요청`에 증거를 채운 뒤 중단한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 공백은 사용자 리뷰 요청 사유가 아니다.

## 배경
현재 `BacktestRunSpec`은 strategy, market, timeframe, 기간만 담고 종목군을 지정할 수 없다. `backtest-ready`의 `input-selector`는 저장된 정제 데이터에서 종목군/기간/frequency를 backtest run input으로 넘기는 경계가 필요하다. 이 subtask는 wire/domain 경계를 먼저 만들고, 저장소 filtering과 operator flow는 후속 subtask에서 연결한다.

## 사용자 리뷰 요청 흐름
구현 중 사용자 전용 결정이나 범위 충돌이 생기면 active `CODE_REVIEW-*-G??.md`의 `사용자 리뷰 요청` 섹션에 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식 그대로 기록한다. 구현 에이전트는 채팅으로 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. code-review가 사용자 리뷰 요청의 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## 분석 결과
### 읽은 파일
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/private/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/update-roadmap/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-test/local/rules.md`
- `agent-test/local/operations-smoke.md`
- `agent-test/local/worker-smoke.md`
- `agent-test/local/contracts-smoke.md`
- `agent-test/local/domain-model-smoke.md`
- `agent-test/local/api-smoke.md`
- `agent-ops/rules/project/domain/operations/rules.md`
- `agent-ops/rules/project/domain/worker/rules.md`
- `agent-ops/rules/project/domain/contracts/rules.md`
- `agent-ops/rules/project/domain/domain-model/rules.md`
- `agent-ops/rules/project/domain/api/rules.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/backtest-loop/PHASE.md`
- `agent-roadmap/phase/backtest-loop/milestones/backtest-data-collection-infrastructure.md`
- `packages/contracts/proto/alt/v1/backtest.proto`
- `packages/contracts/proto/alt/v1/market.proto`
- `packages/domain/backtest/types.go`
- `packages/domain/backtest/types_test.go`
- `packages/domain/market/types.go`
- `services/worker/internal/socket/backtest_mapping.go`
- `services/worker/internal/socket/backtest_mapping_test.go`
- `services/worker/internal/socket/backtest.go`
- `services/worker/internal/socket/backtest_test.go`
- `services/api/internal/socket/backtest.go`
- `services/api/internal/socket/backtest_test.go`
- `services/api/internal/workerclient/client.go`
- `services/api/internal/workerclient/client_test.go`
- `apps/cli/internal/operator/scenario.go`
- `apps/cli/internal/operator/scenario_test.go`
- `apps/cli/internal/operator/runner.go`
- `apps/cli/internal/operator/runner_backtest_test.go`
- `apps/cli/internal/operator/client.go`
- `apps/cli/internal/operator/client_test.go`

### 테스트 환경 규칙
- `test_env=local`로 판단했다.
- `agent-test/local/rules.md`가 존재해 읽었다.
- 매칭 profile로 `operations-smoke.md`, `worker-smoke.md`, `contracts-smoke.md`, `domain-model-smoke.md`, `api-smoke.md`를 읽었다.
- 계약 변경이 있으므로 `bin/contracts-gen` 후 `bin/contracts-check`를 사용한다.
- Go 검증은 fresh 실행이 필요하므로 `go test -count=1`만 허용하고 test cache 출력은 허용하지 않는다.
- 현재 기준으로 `bin/contracts-check`와 통합 `go test -count=1 ./packages/domain/... ./services/worker/internal/backtest ./services/worker/internal/jobs ./services/worker/internal/socket ./services/api/internal/socket ./services/api/internal/workerclient ./apps/cli/internal/operator ./apps/cli/internal/cli` 실행 가능함을 확인했다.
- `agent-contract/index.md`는 없어서 계약 포인터 문서는 적용하지 않았다.

### 테스트 커버리지 공백
- 새 public proto field는 generated Go, worker socket mapping, API/workerclient round-trip 정상/경계 테스트가 필요하다.
- selector가 실제 저장소 bar를 줄이는 동작은 이 subtask에서 구현하지 않는다. 그 공백은 `02+01_storage_bar_selector`가 닫는다.
- operator scenario가 named universe를 start request selector로 바꾸는 흐름은 이 subtask에서 구현하지 않는다. 그 공백은 `03+01,02_backtest_selector_flow`가 닫는다.

### 심볼 참조
- renamed/removed symbol: none.
- 기존 `BacktestRunSpec` 참조: `packages/contracts/proto/alt/v1/backtest.proto:19`, `packages/contracts/proto/alt/v1/paper_trading.proto:18`, `packages/domain/backtest/types.go:25`, `services/worker/internal/socket/backtest_mapping.go:20`, `services/worker/internal/socket/backtest_mapping.go:144`, `services/worker/internal/jobs/backtest_starter.go:66`, `services/worker/internal/jobs/backtest_jobs.go:99`, `apps/cli/internal/operator/runner.go:248`, `services/api/internal/workerclient/client.go:168`.

### 분할 판단
- split decision policy를 평가했고 multi-plan을 선택했다.
- shared task group: `agent-task/m-backtest-data-collection-infrastructure/`
- 이 subtask: `01_input_selector_contract`, predecessor 없음.
- sibling: `02+01_storage_bar_selector`, predecessor `01_input_selector_contract` complete.log 필요, 현재 missing.
- sibling: `03+01,02_backtest_selector_flow`, predecessor `01_input_selector_contract`, `02+01_storage_bar_selector` complete.log 필요, 현재 missing.

### 범위 결정 근거
- 실제 bar filtering, storage source selector resolution, engine execution filtering은 제외한다. 계약 shape가 먼저 안정되어야 한다.
- CLI scenario `start_backtest`에서 universe를 selector로 전환하는 작업은 제외한다. operator flow는 selector가 실행까지 연결된 뒤 검증해야 한다.
- postgres schema 변경은 하지 않는다. run spec의 기존 DB columns는 strategy/market/timeframe/from/to만 저장하고, selector persistence 여부는 후속 작업에서 필요한 경우 별도 판단한다.

### 빌드 등급
- build lane: `cloud-G06`. public proto, generated code, worker/API/CLI test surface를 건드리는 cross-boundary 변경이므로 G06로 잡는다.

## 구현 체크리스트
- [ ] `BacktestRunSpec`에 additive selector message/field를 추가하고 generated Go를 갱신한다.
- [ ] domain `RunSpec`과 worker socket mapping이 selector를 정상/경계 케이스로 왕복하도록 구현한다.
- [ ] API/workerclient/CLI request 생성 테스트가 selector 필드 보존을 확인하도록 갱신한다.
- [ ] `bin/contracts-gen`, `bin/contracts-check`, 지정 `go test -count=1` 검증을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] Proto Selector Shape
#### 문제
`BacktestRunSpec`이 기간과 frequency만 표현하고 종목군 selector를 담을 필드가 없다.

```proto
// packages/contracts/proto/alt/v1/backtest.proto:19
message BacktestRunSpec {
  string strategy_id = 1;
  Market market = 2;
  Timeframe timeframe = 3;
  int64 from_unix_ms = 4;
  int64 to_unix_ms = 5;
}
```

#### 해결 방법
기존 field number를 유지하고 additive field만 추가한다. selector가 비어 있으면 기존 full-market 동작을 보존한다.

```proto
message BacktestInputSelector {
  repeated string instrument_ids = 1;
  repeated string symbols = 2;
}

message BacktestRunSpec {
  string strategy_id = 1;
  Market market = 2;
  Timeframe timeframe = 3;
  int64 from_unix_ms = 4;
  int64 to_unix_ms = 5;
  BacktestInputSelector selector = 6;
}
```

#### 수정 파일 및 체크리스트
- [ ] `packages/contracts/proto/alt/v1/backtest.proto`에 `BacktestInputSelector`와 `BacktestRunSpec.selector = 6`을 추가한다.
- [ ] `bin/contracts-gen`으로 `packages/contracts/gen/go/alt/v1/backtest.pb.go`를 갱신한다.
- [ ] parser map에는 새 message type을 직접 주고받지 않으므로 등록을 추가하지 않는다.

#### 테스트 작성
- generated contract shape는 `bin/contracts-check`로 확인한다.
- 별도 proto-only unit test는 작성하지 않는다. field behavior는 mapping/round-trip 테스트에서 검증한다.

#### 중간 검증
```bash
bin/contracts-gen
bin/contracts-check
```
예상 결과: 두 명령 모두 exit 0.

### [API-2] Domain And Worker Mapping
#### 문제
worker socket mapping은 proto spec을 domain `RunSpec`으로 변환하지만 selector를 담을 domain field가 없다.

```go
// packages/domain/backtest/types.go:25
type RunSpec struct {
	StrategyID StrategyID
	Market     market.Market
	Timeframe  market.Timeframe
	From       time.Time
	To         time.Time
}
```

```go
// services/worker/internal/socket/backtest_mapping.go:51
return backtest.RunSpec{
	StrategyID: backtest.StrategyID(spec.GetStrategyId()),
	Market:     mkt,
	Timeframe:  timeframe,
	From:       from,
	To:         to,
}, nil
```

#### 해결 방법
domain에 `InputSelector`를 추가하고 mapping에서 trim/blank 검증 후 복사한다. `runSpecToProto`도 selector를 되돌려 run 조회 응답에서 손실이 없게 한다.

```go
type InputSelector struct {
	InstrumentIDs []market.InstrumentID
	Symbols        []string
}

type RunSpec struct {
	StrategyID StrategyID
	Market     market.Market
	Timeframe  market.Timeframe
	From       time.Time
	To         time.Time
	Selector   InputSelector
}
```

#### 수정 파일 및 체크리스트
- [ ] `packages/domain/backtest/types.go`에 `InputSelector`와 `RunSpec.Selector`를 추가한다.
- [ ] `services/worker/internal/socket/backtest_mapping.go`에 selector from/to proto helper를 추가한다.
- [ ] `services/worker/internal/socket/backtest_mapping_test.go`에 정상 selector round-trip과 blank `instrument_ids`/`symbols` reject 케이스를 추가한다.
- [ ] `packages/domain/backtest/types_test.go`는 domain helper를 추가한 경우에만 갱신한다.

#### 테스트 작성
- `TestRunSpecFromProtoValid`에 `Selector` assertion을 추가한다.
- `TestRunSpecFromProtoRejectsInvalid`에 blank selector entries를 추가한다.
- `TestRunToProto`에 selector가 proto로 보존되는 assertion을 추가한다.

#### 중간 검증
```bash
go test -count=1 ./packages/domain/... ./services/worker/internal/socket
```
예상 결과: selector mapping 테스트 포함 exit 0.

### [API-3] Forwarding And Client Request Preservation
#### 문제
API는 start request를 worker로 그대로 전달하고 CLI runner는 `BacktestRunSpec`을 직접 구성한다. selector field가 추가되면 request 생성과 forwarding이 손실 없이 유지되는지 확인해야 한다.

```go
// apps/cli/internal/operator/runner.go:248
Spec: &altv1.BacktestRunSpec{
	StrategyId: step.Request.StrategyID,
	Market:     marketByName[step.Request.Market],
	Timeframe:  timeframeByName[step.Request.Timeframe],
	FromUnixMs: step.Request.FromUnixMs,
	ToUnixMs:   step.Request.ToUnixMs,
},
```

#### 해결 방법
이 subtask에서는 CLI schema를 확장하지 않는다. 대신 API/workerclient tests가 generated selector를 보존하는지 확인하고, CLI request 생성부는 후속 plan에서 selector를 채울 수 있도록 기존 구조를 유지한다.

#### 수정 파일 및 체크리스트
- [ ] `services/api/internal/socket/backtest_test.go`의 `validStart` 또는 forwarding test에 selector field를 포함해 worker fake가 받은 request를 assertion한다.
- [ ] `services/api/internal/workerclient/client_test.go`의 `TestWorkerClient_StartBacktest_Success`에 selector round-trip assertion을 추가한다.
- [ ] `apps/cli/internal/operator/client_test.go`의 start backtest request capture가 새 field를 깨뜨리지 않는지 필요한 assertion을 추가한다.

#### 테스트 작성
- public wire field 보존 테스트이므로 정상 round-trip 테스트를 작성한다.
- boundary validation은 worker socket mapping test에서 담당하므로 API handler에는 중복 validation을 추가하지 않는다.

#### 중간 검증
```bash
go test -count=1 ./services/api/internal/socket ./services/api/internal/workerclient ./apps/cli/internal/operator
```
예상 결과: selector field가 start request에서 손실되지 않고 exit 0.

## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `packages/contracts/proto/alt/v1/backtest.proto` | API-1 |
| `packages/contracts/gen/go/alt/v1/backtest.pb.go` | API-1 |
| `packages/domain/backtest/types.go` | API-2 |
| `packages/domain/backtest/types_test.go` | API-2 |
| `services/worker/internal/socket/backtest_mapping.go` | API-2 |
| `services/worker/internal/socket/backtest_mapping_test.go` | API-2 |
| `services/api/internal/socket/backtest_test.go` | API-3 |
| `services/api/internal/workerclient/client_test.go` | API-3 |
| `apps/cli/internal/operator/client_test.go` | API-3 |

## 최종 검증
```bash
bin/contracts-gen
bin/contracts-check
go test -count=1 ./packages/domain/... ./services/worker/internal/socket ./services/api/internal/socket ./services/api/internal/workerclient ./apps/cli/internal/operator
```
예상 결과: 모든 명령 exit 0. Go test cache 출력은 허용하지 않는다.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
