<!-- task=m-backtest-scenario-automation/01_scenario_matrix plan=0 tag=API -->

# Plan - API Scenario Matrix

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

이 plan의 구현이 끝나면 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용, 검증 출력, 계획 대비 변경 사항으로 채운다. 구현 중 사용자 결정, 사용자 소유 외부 환경, 범위 충돌로 막히면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 결정 필요 내용과 증거를 남기고 멈춘다. 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 명령 재실행이나 산출물 수집으로 해소 가능한 evidence 공백은 사용자 리뷰 요청이 아니다.

## 배경

`Backtest Scenario Automation` 마일스톤은 universe, 기간, frequency/timeframe, strategy 조합을 YAML scenario로 반복 실행할 수 있어야 한다. 현재 `apps/cli/internal/operator/scenario.go`는 단일 `steps` 목록과 named `universes`만 검증하며, 조합 matrix를 표현하거나 결정적으로 펼치는 구조가 없다. 이 subtask는 실행 없이 matrix schema, dry-run validation, expansion helper까지만 만든다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 prompt는 금지이며, code-review가 해당 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/backtest-loop/milestones/backtest-scenario-automation.md`
- Task ids:
  - `scenario-matrix`: universe, 기간, frequency, strategy 조합을 YAML scenario에서 표현하고 검증할 수 있게 한다.
- 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-ops/rules/project/domain/operations/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/operations-smoke.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/backtest-loop/PHASE.md`
- `agent-roadmap/phase/backtest-loop/milestones/backtest-scenario-automation.md`
- `apps/cli/go.mod`
- `apps/cli/internal/operator/scenario.go`
- `apps/cli/internal/operator/scenario_test.go`
- `apps/cli/internal/cli/cli.go`
- `apps/cli/internal/cli/cli_test.go`
- `apps/cli/testdata/operator/backtest_run_request.yaml`
- `apps/cli/testdata/operator/backtest_run_polling.yaml`
- `apps/cli/testdata/operator/backtest_result_summary.yaml`

### 테스트 환경 규칙

- 선택 env: `local`.
- `agent-test/local/rules.md`와 `agent-test/local/operations-smoke.md`를 읽었다.
- 적용 명령: milestone 검증에 명시된 `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`.
- baseline 확인: 위 명령은 현재 checkout에서 exit 0으로 통과했다.
- `<확인 필요>` 값 없음. fallback 없음.

### 테스트 커버리지 공백

- matrix root schema, matrix period validation, matrix expansion ordering: 기존 테스트 없음. 새 tests 필요.
- 기존 universe 참조와 `start_backtest` selector 검증: `scenario_test.go`가 일부 커버한다.
- CLI `validate`의 matrix-only scenario 출력: 기존 테스트 없음. 새 tests 필요.

### 심볼 참조

- renamed/removed symbol: none.

### 분할 판단

- split policy를 먼저 평가했다.
- shared task group: `agent-task/m-backtest-scenario-automation/`.
- `01_scenario_matrix`: matrix schema/validation foundation, depends_on=[].
- `02+01_batch_runner`: matrix 실행 및 CLI output, depends_on=[01].
- `03+01,02_result_handoff`: fixtures/docs/handoff evidence, depends_on=[01,02].
- 이 subtask는 선행 의존성이 없다.

### 범위 결정 근거

- `services/api/**`, `services/worker/**`, `packages/contracts/**`, `packages/domain/**`는 제외한다. 이 subtask는 CLI YAML schema와 validation만 다루며 runtime protobuf 계약을 바꾸지 않는다.
- result fixture와 `headless_validation.md`는 03에서 다룬다.
- batch 실행, polling output 변경은 02에서 다룬다.

### 빌드 등급

- build: `local-G06`, review: `local-G06`.
- 이유: bounded CLI schema/test change이며 deterministic Go tests로 검증 가능하다.

## 구현 체크리스트

- [ ] `Scenario`에 optional backtest matrix schema를 추가하고 matrix-only scenario를 dry-run validation 대상으로 허용한다. 검증: `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`
- [ ] matrix combination expansion helper를 deterministic order로 추가하고 validation tests를 작성한다. 검증: `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`
- [ ] CLI `operator scenario validate`가 matrix-only scenario를 유효하게 보고하는 test를 추가한다. 검증: `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [API-1] Matrix Schema And Validation

### 문제

[scenario.go](/config/workspace/alt/apps/cli/internal/operator/scenario.go:386)는 `Scenario`가 `steps`만 실행 단위로 갖는다. [scenario.go](/config/workspace/alt/apps/cli/internal/operator/scenario.go:426)는 `len(s.Steps) == 0`이면 무조건 오류를 반환해 matrix-only YAML을 표현할 수 없다.

### 해결 방법

`Scenario`에 optional `Matrix *BacktestScenarioMatrix`를 추가하고, `steps`가 비어 있어도 matrix가 있으면 유효하게 둔다. Matrix는 named universe, strategy, timeframe, period 조합을 표현한다.

Before:

```go
// apps/cli/internal/operator/scenario.go:386
type Scenario struct {
	Name      string           `yaml:"name"`
	Timeout   Duration         `yaml:"timeout"`
	Universes []UniverseConfig `yaml:"universes"`
	Steps     []Step           `yaml:"steps"`
}
```

After:

```go
type Scenario struct {
	Name      string                  `yaml:"name"`
	Timeout   Duration                `yaml:"timeout"`
	Universes []UniverseConfig        `yaml:"universes"`
	Matrix    *BacktestScenarioMatrix `yaml:"matrix"`
	Steps     []Step                  `yaml:"steps"`
}

type BacktestScenarioMatrix struct {
	Universes  []string       `yaml:"universes"`
	Strategies []string       `yaml:"strategies"`
	Timeframes []string       `yaml:"timeframes"`
	Periods    []MatrixPeriod `yaml:"periods"`
}

type MatrixPeriod struct {
	ID         string `yaml:"id"`
	FromUnixMs int64 `yaml:"from_unix_ms"`
	ToUnixMs   int64 `yaml:"to_unix_ms"`
}
```

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

- [ ] `apps/cli/internal/operator/scenario.go`: matrix structs 추가.
- [ ] `apps/cli/internal/operator/scenario.go`: `Validate`에서 `steps` 또는 `matrix` 중 하나를 요구하도록 변경.
- [ ] `apps/cli/internal/operator/scenario_test.go`: matrix-only valid fixture test 추가.

### 테스트 작성

- 작성: `TestValidateBacktestScenarioMatrix`로 `matrix.universes/strategies/timeframes/periods`가 parse되고 유효한지 검증한다.
- 작성: `TestValidateRejectsInvalidBacktestScenarioMatrix`로 empty universe, unknown universe, invalid timeframe, reversed period를 검증한다.

### 중간 검증

```bash
go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli
```

예상: 두 package 모두 `ok`.

## [API-2] Deterministic Matrix Expansion

### 문제

[runner.go](/config/workspace/alt/apps/cli/internal/operator/runner.go:129)는 이미 `steps`를 순서대로 실행하지만, matrix 조합을 실행 단위로 펼치는 helper가 없다. 02 subtask가 실행을 맡으려면 01에서 source-of-truth expansion 순서를 제공해야 한다.

### 해결 방법

`Scenario.MatrixRuns() ([]MatrixRun, error)`를 추가한다. 순서는 universe -> strategy -> timeframe -> period로 고정한다. `MatrixRun.ID`는 `<universe>__<strategy>__<timeframe>__<period>` 형태로 만들고 공백은 validation에서 거부한다.

Before:

```go
// apps/cli/internal/operator/scenario.go:420
// Validate runs the dry-run checks: a named scenario, at least one step, and
// every step having a unique id and a known action.
func (s *Scenario) Validate() error {
```

After:

```go
type MatrixRun struct {
	ID         string
	Universe   string
	StrategyID string
	Timeframe  string
	FromUnixMs int64
	ToUnixMs   int64
}

func (s *Scenario) MatrixRuns() ([]MatrixRun, error) {
	// Called after Validate, but still returns errors so tests can exercise it directly.
}
```

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

- [ ] `apps/cli/internal/operator/scenario.go`: `MatrixRun`과 `MatrixRuns` 추가.
- [ ] `apps/cli/internal/operator/scenario.go`: matrix validation에서 duplicate generated ID를 거부.
- [ ] `apps/cli/internal/operator/scenario_test.go`: expansion order와 generated IDs 검증.

### 테스트 작성

- 작성: `TestBacktestScenarioMatrixRunsExpandsInDeterministicOrder`.

### 중간 검증

```bash
go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli
```

예상: 두 package 모두 `ok`.

## [API-3] CLI Validate Output For Matrix

### 문제

[cli.go](/config/workspace/alt/apps/cli/internal/cli/cli.go:86)는 validate 성공 시 `steps=%d`만 출력한다. matrix-only scenario가 유효해지면 operator가 조합 수를 볼 수 있어야 한다.

### 해결 방법

`operator scenario validate` 출력에 matrix가 있으면 `matrix_runs=<n>`을 추가한다. 기존 step-only scenario output은 유지한다.

Before:

```go
// apps/cli/internal/cli/cli.go:86
fmt.Fprintf(stdout, "scenario=%s status=valid steps=%d\n", sc.Name, len(sc.Steps))
```

After:

```go
runs, err := sc.MatrixRuns()
if err != nil {
	fmt.Fprintf(stderr, "validate failed: %v\n", err)
	return exitParseError
}
if len(runs) > 0 {
	fmt.Fprintf(stdout, "scenario=%s status=valid steps=%d matrix_runs=%d\n", sc.Name, len(sc.Steps), len(runs))
	return exitOK
}
fmt.Fprintf(stdout, "scenario=%s status=valid steps=%d\n", sc.Name, len(sc.Steps))
```

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

- [ ] `apps/cli/internal/cli/cli.go`: validate output 보강.
- [ ] `apps/cli/internal/cli/cli_test.go`: matrix-only validate output test 추가.

### 테스트 작성

- 작성: `TestRunScenarioValidateMatrix`에서 `matrix_runs=...` 포함을 검증한다.

### 중간 검증

```bash
go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli
```

예상: 두 package 모두 `ok`.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/cli/internal/operator/scenario.go` | API-1, API-2 |
| `apps/cli/internal/operator/scenario_test.go` | API-1, API-2 |
| `apps/cli/internal/cli/cli.go` | API-3 |
| `apps/cli/internal/cli/cli_test.go` | API-3 |

## 최종 검증

```bash
go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli
```

예상: 두 package 모두 `ok`; Go test cache는 fresh 실행이 필요하므로 `-count=1`을 유지한다.

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