<!-- task=m-backtest-multi-timeframe-coverage/02+01_minute_import_rejection plan=0 tag=API -->

# Plan - Minute Import Typed Rejection

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

구현 마지막 단계는 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채우는 것이다. 구현 후 active 파일을 그대로 두고 리뷰 준비를 보고한다. 종결, archive 이동, `complete.log` 작성은 code-review 전용이다.

선택된 SDD 결정 또는 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 막을 때만 review stub의 `사용자 리뷰 요청` 섹션을 채우고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경/secret/서비스 차단, 일반 범위 조정, 검증 공백은 사용자 리뷰 요청이 아니라 `검증 결과` 또는 `계획 대비 변경 사항`에 기록한다.

## 배경

KIS daily importer capability matrix는 monthly/minute를 이미 unsupported로 판단하지만, `import_daily_bars` socket/CLI/job 요청 자체에는 timeframe을 실을 수 없어 분봉 import 거부를 stable fixture로 남기기 어렵다. `01_import_timeframe_contract`가 proto field를 추가한 뒤, 이 계획은 CLI scenario부터 worker boundary까지 timeframe을 전달하고 `minute_1`/`minute_5`를 typed `invalid_request`로 거부한다. 실제 분봉 provider import 구현은 이번 plan 범위가 아니며 SDD S03은 typed rejection evidence로 충족한다.

## 사용자 리뷰 요청 흐름

사용자 리뷰 요청은 active `CODE_REVIEW-*-G??.md`의 `사용자 리뷰 요청` 섹션에만 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 내용을 따른다. 직접 사용자 프롬프트는 금지하며, code-review가 요청 타당성을 검증하고 실제 `USER_REVIEW.md` 작성을 소유한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/backtest-loop/milestones/backtest-multi-timeframe-coverage.md`
- Task ids:
  - `minute-ingest`: 분봉 provider import 경계를 추가하거나, provider 미지원 조합을 typed error로 거부한다.
- Completion mode: check-on-pass

## Spec Targets

- SDD: `agent-roadmap/sdd/backtest-loop/backtest-multi-timeframe-coverage/SDD.md`
- Acceptance scenarios:
  - `S03`: task=`minute-ingest`; evidence=`minute import success 또는 typed reject fixture`
- Completion mode: spec-check-on-pass

## 분석 결과

### 읽은 파일

- `AGENTS.md`
- `agent-roadmap/phase/backtest-loop/milestones/backtest-multi-timeframe-coverage.md`
- `agent-roadmap/sdd/backtest-loop/backtest-multi-timeframe-coverage/SDD.md`
- `agent-test/local/rules.md`
- `agent-test/local/contracts-smoke.md`
- `agent-test/local/api-smoke.md`
- `agent-test/local/worker-smoke.md`
- `agent-test/local/operations-smoke.md`
- `packages/contracts/proto/alt/v1/market.proto`
- `apps/cli/internal/operator/scenario.go`
- `apps/cli/internal/operator/runner.go`
- `apps/cli/internal/operator/scenario_test.go`
- `apps/cli/internal/operator/runner_market_test.go`
- `apps/cli/internal/operator/client.go`
- `apps/cli/internal/operator/client_test.go`
- `services/worker/internal/socket/market.go`
- `services/worker/internal/socket/market_test.go`
- `services/worker/internal/socket/backtest_mapping.go`
- `services/worker/internal/jobs/marketdata_jobs.go`
- `services/worker/internal/jobs/marketdata_jobs_test.go`
- `services/worker/internal/providers/kis/daily_itemchartprice.go`
- `services/worker/internal/providers/kis/daily_itemchartprice_test.go`

### 테스트 환경 규칙

- `test_env=local`.
- `contracts-smoke`: `ImportDailyBarsRequest.timeframe` generated contract가 선행되어야 하므로 `bin/contracts-check`를 최종 확인에 포함한다.
- `api-smoke`: proto-socket request boundary가 바뀌므로 `go test ./services/api/...`를 포함한다.
- `worker-smoke`: worker socket/jobs 거부 경계를 변경하므로 `go test ./services/worker/...`를 포함한다.
- `operations-smoke`: fixture/generator drift 확인용으로 `git diff --check`를 포함한다.
- fallback 없음. `<확인 필요>` 값 없음.

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

- CLI validation은 import action의 `timeframe`을 아직 검사하지 않는다. `scenario_test.go`에 invalid timeframe dry-run case와 daily default/valid minute pass-through case를 추가한다.
- CLI runner는 `ImportDailyBarsRequest`에 timeframe을 아직 채우지 않는다. `runner_market_test.go`에서 `minute_1`/`minute_5` mapping을 확인한다.
- Worker socket `handleImportDailyBars`는 timeframe을 아직 보지 않는다. `market_test.go`에서 minute request가 `invalid_request`이고 importer가 호출되지 않는지 확인한다.
- Worker job payload는 hardcoded daily capability check만 한다. `marketdata_jobs_test.go`에서 optional `timeframe`과 minute rejection을 확인한다.
- Stable JSONL/text fixture는 기존 typed-error event 평가가 있으나 분봉 import fixture는 없다. CLI operator fixture를 추가한다.

### 심볼 참조

- renamed/removed symbol 없음.
- 추가 field call sites:
  - `apps/cli/internal/operator/runner.go:257` `ActionImportDailyBars` request 생성.
  - `services/worker/internal/socket/market.go:69` `handleImportDailyBars`.
  - `services/worker/internal/jobs/marketdata_jobs.go:27` `DailyBarImportPayload`.
  - `services/worker/internal/jobs/marketdata_jobs.go:113` capability check.

### 분할 판단

- split policy를 plan 작성 전에 평가했다.
- 공유 task group: `agent-task/m-backtest-multi-timeframe-coverage/`
- sibling split:
  - `01_import_timeframe_contract`: 선행 proto field/generation.
  - `02+01_minute_import_rejection`: 이 plan. predecessor `01` complete.log 필요.
  - `03_monthly_aggregation`: 독립 monthly aggregation.
- predecessor 상태: `01`은 archived `complete.log`로 충족됐다. 근거: `agent-task/archive/2026/06/m-backtest-multi-timeframe-coverage/01_import_timeframe_contract/complete.log`.

### 범위 결정 근거

- 실제 KIS minute provider endpoint 연동은 제외한다. Milestone과 SDD가 typed rejection을 허용한다.
- 월봉은 이 plan에서 daily fallback 또는 rejection으로 처리하지 않는다. 월봉 source of truth는 `03_monthly_aggregation`이다.
- `ImportDailyBarsRequest` field 생성은 predecessor plan 소유다. 이 plan은 generated field가 존재한다고 가정한다.

### 빌드 등급

- `local-G06`: public request field propagation, CLI/worker/job boundary, stable fixture가 함께 바뀌지만 동작은 typed rejection으로 제한되어 로컬 테스트로 닫힌다.

## 의존 관계 및 구현 순서

- 이 subtask directory의 `02+01_...`가 runtime source of truth다.
- `01_import_timeframe_contract`는 `agent-task/archive/2026/06/m-backtest-multi-timeframe-coverage/01_import_timeframe_contract/complete.log`로 PASS 완료가 확인됐다. 이 plan은 generated `ImportDailyBarsRequest.Timeframe` accessor가 존재하는 상태에서 구현한다.

## 구현 체크리스트

- [ ] `apps/cli/internal/operator/scenario.go`에서 `import_daily_bars`의 optional `request.timeframe`을 검증한다. 빈 값은 daily default, 값이 있으면 `validTimeframes`만 허용한다.
- [ ] `apps/cli/internal/operator/runner.go`에서 `ImportDailyBarsRequest.Timeframe`을 채운다. 빈 값은 `TIMEFRAME_DAILY`, 명시 값은 `timeframeByName`을 사용한다.
- [ ] `services/worker/internal/socket/market.go`에서 `ImportDailyBarsRequest.timeframe`을 daily default로 해석하고, daily가 아니면 typed `invalid_request`로 거부하며 importer를 호출하지 않는다.
- [ ] `services/worker/internal/jobs/marketdata_jobs.go`에 optional payload `timeframe`을 추가하고 job handler도 daily 외 timeframe을 capability/rejection error로 막는다.
- [ ] CLI/worker/job tests와 stable JSONL/text fixture를 추가한다. 검증: minute import rejected fixture가 stable text/JSONL로 남는다.
- [ ] `bin/contracts-check`, `go test ./apps/cli/internal/operator ./services/worker/internal/socket ./services/worker/internal/jobs`, `go test ./services/api/...`, `go test ./services/worker/...`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] CLI Import Timeframe Propagation

#### 문제

`apps/cli/internal/operator/scenario.go:290`의 comment는 timeframe을 `list_bars` 전용으로 설명하고, `scenario.go:967`의 import validation은 timeframe을 검사하지 않는다. `runner.go:257`의 `ImportDailyBarsRequest` 생성도 timeframe을 보내지 않는다.

Before:

```go
290 // Timeframe selects the bar timeframe for a list_bars query.
291 Timeframe string `yaml:"timeframe"`
...
967 case ActionImportDailyBars:
968     if step.Request.Provider == "" {
```

```go
257 case ActionImportDailyBars:
258     resp, err := client.ImportDailyBars(stepCtx, &altv1.ImportDailyBarsRequest{
259         Provider:     step.Request.Provider,
260         SelectorKind: step.Request.SelectorKind,
```

#### 해결 방법

comment를 import/list/freshness/backtest 공용 의미로 고치고, import validation에서 `step.Request.Timeframe != "" && !validTimeframes[...]`면 bad input으로 막는다. runner는 helper로 `importTimeframe := altv1.Timeframe_TIMEFRAME_DAILY`를 기본값으로 두고 명시 값은 `timeframeByName`을 사용한다.

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

- [ ] `apps/cli/internal/operator/scenario.go`: comment와 import validation 추가.
- [ ] `apps/cli/internal/operator/runner.go`: import request timeframe mapping 추가.
- [ ] `apps/cli/internal/operator/scenario_test.go`: invalid timeframe, valid `minute_1`, empty default validation case.
- [ ] `apps/cli/internal/operator/runner_market_test.go`: request timeframe mapping assertion.

#### 테스트 작성

- `TestValidateImportDailyBars` 계열에 optional timeframe 검증을 추가한다.
- `TestRunScenarioImportDailyBarsSuccess` 또는 새 test에서 empty default daily와 `minute_1` request field를 확인한다.

#### 중간 검증

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

기대 결과: operator package 통과.

### [API-2] Worker Socket Typed Rejection

#### 문제

`services/worker/internal/socket/market.go:69`의 `handleImportDailyBars`는 request timeframe을 읽지 않고 `DailyBarImporter`를 호출한다. minute request가 들어와도 daily importer로 조용히 흘러갈 수 있다.

Before:

```go
87 from, to, err := parseImportDateRange(req.GetFromYyyymmdd(), req.GetToYyyymmdd())
88 if err != nil {
89     return &altv1.ImportDailyBarsResponse{Error: marketInvalidRequest(err.Error())}, nil
90 }
91 if deps.DailyBarImporter == nil {
```

#### 해결 방법

`req.GetTimeframe()`이 unspecified면 daily로 default한다. 명시 값은 `timeframeFromProto`로 변환하고 daily가 아니면 `ImportDailyBarsResponse{Error: marketInvalidRequest("import_daily_bars supports timeframe daily; got <value>")}`를 반환한다. 이 거부는 importer nil check보다 먼저 둬서 provider 미지원 조합을 typed input error로 안정화한다.

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

- [ ] `services/worker/internal/socket/market.go`: import timeframe normalization/rejection helper 추가.
- [ ] `services/worker/internal/socket/market_test.go`: `TIMEFRAME_MINUTE_1`, `TIMEFRAME_MINUTE_5` rejection과 importer not-called assertion.

#### 테스트 작성

- `TestHandleImportDailyBarsValidation` 또는 새 `TestHandleImportDailyBarsRejectsUnsupportedTimeframe`을 추가한다.
- error code는 `invalid_request`, message는 stable prefix를 assert한다.

#### 중간 검증

```bash
go test ./services/worker/internal/socket
```

기대 결과: socket package 통과.

### [API-3] Worker Job Payload Boundary

#### 문제

`services/worker/internal/jobs/marketdata_jobs.go:27`의 `DailyBarImportPayload`에는 timeframe이 없고, `marketdata_jobs.go:113`은 capability check에 `market.TimeframeDaily`를 hardcode한다.

Before:

```go
27 type DailyBarImportPayload struct {
28     Provider     string   `json:"provider"`
...
35     To           string   `json:"to,omitempty"`
}
...
113 decision := capability.CheckBars(market.ProviderCapabilityRequest{
114     Provider:  market.Provider(p.Provider),
115     Market:    market.Market(p.Market),
116     Venue:     market.Venue(p.Venue),
117     Timeframe: market.TimeframeDaily,
118 })
```

#### 해결 방법

`Timeframe string json:"timeframe,omitempty"`를 payload에 추가한다. empty는 `market.TimeframeDaily`, 명시 값은 `market.Timeframe(...)`로 해석하되 daily가 아니면 capability decision 또는 explicit error로 importer 전에 거부한다. error text는 fixture에서 안정적으로 비교할 수 있게 prefix를 고정한다.

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

- [ ] `services/worker/internal/jobs/marketdata_jobs.go`: payload field와 timeframe normalization.
- [ ] `services/worker/internal/jobs/marketdata_jobs_test.go`: empty daily compatibility, `1m`/`5m` rejection, importer not-called.

#### 테스트 작성

- 기존 `TestRegisterDailyBarImportHandlerRejectsTimeframeViaCapabilityMatrix`를 실제 payload timeframe rejection test로 보강하거나 새 test를 추가한다.

#### 중간 검증

```bash
go test ./services/worker/internal/jobs
```

기대 결과: jobs package 통과.

### [API-4] Stable Minute Rejection Fixture

#### 문제

SDD S03은 minute import success 또는 typed reject fixture를 요구한다. 현재 operator fixture에는 `minute_1`/`minute_5` import rejection을 보여주는 stable JSONL/text가 없다.

#### 해결 방법

기존 operator fixture 위치와 naming을 따른다. `import_daily_bars` step에 `timeframe: minute_1` 또는 `minute_5`, `expect.status: error`, `expect.error_code: invalid_request`를 둔다. JSONL expected fixture는 `status=ok` step event가 typed error를 기대대로 받았음을 보여야 한다.

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

- [ ] 기존 operator fixture 디렉터리의 import/freshness fixture naming을 확인한다.
- [ ] minute import rejected scenario YAML 추가.
- [ ] expected JSONL/text fixture 추가.
- [ ] handoff/runner fixture test에 새 fixture를 연결한다.

#### 테스트 작성

- fixture를 읽어 runner dry/run 또는 fixture comparison test가 stable output을 비교하게 한다.

#### 중간 검증

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

기대 결과: 새 fixture test 포함 통과.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/cli/internal/operator/scenario.go` | API-1 |
| `apps/cli/internal/operator/runner.go` | API-1 |
| `apps/cli/internal/operator/scenario_test.go` | API-1 |
| `apps/cli/internal/operator/runner_market_test.go` | API-1 |
| `services/worker/internal/socket/market.go` | API-2 |
| `services/worker/internal/socket/market_test.go` | API-2 |
| `services/worker/internal/jobs/marketdata_jobs.go` | API-3 |
| `services/worker/internal/jobs/marketdata_jobs_test.go` | API-3 |
| operator scenario/expected fixture files | API-4 |

## 최종 검증

```bash
bin/contracts-check
go test ./apps/cli/internal/operator ./services/worker/internal/socket ./services/worker/internal/jobs
go test ./services/api/...
go test ./services/worker/...
git diff --check
```

기대 결과: 모든 명령 exit code 0. Go test cache output은 허용하지만, fixture 변경 검증이 의심되면 해당 package는 `-count=1`로 재실행하고 변경 사유를 review stub에 기록한다.

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