<!-- task=m-backtest-data-collection-infrastructure/03+02_gap_handling plan=0 tag=DATASET_GAP -->

# PLAN-cloud-G06 - Dataset Gap Handling Status

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

구현 완료의 마지막 단계는 active `CODE_REVIEW-cloud-G06.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이다. 검증 명령을 실행하고 실제 stdout/stderr를 기록한 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log` 작성, archive 이동은 code-review-skill 전용이다.

구현 중 사용자만 결정할 수 있는 정책, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 충돌 때문에 안전하게 진행할 수 없으면 `CODE_REVIEW-cloud-G06.md`의 `사용자 리뷰 요청` 섹션에 정확한 결정 필요 사항, 근거, 실행한 명령과 출력, 재개 조건을 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 채팅 선택지를 제시하거나 `request_user_input`을 호출하거나, `USER_REVIEW.md`, `complete.log`, archive log를 직접 만들지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 증거 공백은 사용자 리뷰 요청이 아니다.

## 배경

현재 `collection_freshness`는 universe 안의 symbol별 bar 존재 여부와 최신 일자만 출력한다. 백테스트 입력으로 쓰려면 특정 거래일 결측, 중복 bar, provider 지연이 freshness와 별도로 드러나야 한다. 이 작업은 기존 operator scenario 계약을 확장해 headless YAML/JSONL/text 출력에서 data quality 상태를 검증할 수 있게 한다.

## 사용자 리뷰 요청 흐름

구현 중 차단 사항은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `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-data-collection-infrastructure.md`
- Task ids:
  - `gap-handling`: 결측, 휴장일, 중복, provider 지연을 백테스트 입력에서 식별할 수 있게 한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-ops/skills/common/plan/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/domain-model-smoke.md`
- `agent-test/local/worker-smoke.md`
- `agent-ops/rules/private/kis-api-local-cache.md`
- `agent-roadmap/phase/backtest-loop/milestones/backtest-data-collection-infrastructure.md`
- `agent-task/archive/2026/06/m-backtest-data-collection-infrastructure/02+01_freshness_status/complete.log`
- `apps/cli/internal/operator/scenario.go`
- `apps/cli/internal/operator/runner.go`
- `apps/cli/internal/operator/output.go`
- `apps/cli/internal/operator/runner_market_test.go`
- `apps/cli/internal/operator/scenario_test.go`
- `apps/cli/internal/operator/handoff_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/expected/market_data_status_query.jsonl`
- `apps/cli/testdata/operator/expected/us_market_data_status_query.jsonl`
- `apps/cli/testdata/operator/headless_validation.md`

### 테스트 환경 규칙

`test_env=local`을 적용했다. `agent-test/local/rules.md`가 존재해 읽었고, 관련 프로필로 `operations-smoke.md`, `domain-model-smoke.md`, `worker-smoke.md`를 읽었다. 이번 계획의 직접 검증은 operator headless scenario 계약이므로 `operations-smoke.md`의 CLI/operator 중심 검증을 적용하고, 최종 명령은 기존 freshness 작업과 같은 `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`를 사용한다. `<확인 필요>` 값이나 구조적으로 빈 테스트 규칙은 없었다.

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

- scenario schema: `expect.freshness_status`, `missing_count`, `latest_yyyymmdd` 검증은 있으나 `request.expected_yyyymmdd`, `expect.gap_status`, `gap_count`, `duplicate_count`, `provider_delay_days` 검증은 없다. 새 validation 단위 테스트가 필요하다.
- runner freshness: `fresh`, `missing`, `error` 케이스는 `runner_market_test.go`에 있으나 trading date gap, duplicate daily bar, provider delay 판정은 없다. 새 runner 테스트가 필요하다.
- output/handoff: 기존 JSONL/text/handoff matrix는 `freshness_status`, `missing_count`, `latest_yyyymmdd`만 약속한다. 새 출력 key와 fixture 검증 업데이트가 필요하다.

### 심볼 참조

renamed/removed symbol은 없다. `collection_freshness`, `FreshnessStatus`, `MissingSymbols`, `LatestYYYYMMDD` 참조는 `apps/cli/internal/operator/scenario.go`, `runner.go`, `output.go`, `runner_market_test.go`, `scenario_test.go`, `handoff_test.go`, `apps/cli/testdata/operator/*market_data_status_query*`, `apps/cli/testdata/operator/headless_validation.md`에 분포한다.

### 분할 판단

split decision policy를 먼저 평가했다. 이 작업은 기존 `freshness-status` 구현 위의 call-site 확장이므로 `03+02_gap_handling` 단일 dependent subtask로 둔다. 선행 index `02`는 `agent-task/archive/2026/06/m-backtest-data-collection-infrastructure/02+01_freshness_status/complete.log`로 충족된다.

별도 API/foundation subtask는 만들지 않는다. protobuf, API service, worker storage schema를 바꾸지 않고 CLI operator scenario schema와 output event만 확장하기 때문이다. 테스트 전략도 scenario validation, runner, output fixture가 같은 operator package 안에서 한 번에 검토 가능하고, 분리하면 fixture/output 계약 조율 비용이 더 커진다.

### 범위 결정 근거

포함 범위는 `apps/cli/internal/operator`의 scenario schema/validation, `collection_freshness` runner 계산, output serialization/text 렌더링, operator fixtures, handoff matrix, 관련 테스트다. `packages/contracts`, `services/api`, `services/worker`, PostgreSQL schema/migration, provider adapter 구현, 실제 휴장일 캘린더 연동은 제외한다. 휴장일은 이번 작업에서 `request.expected_yyyymmdd`가 명시한 거래일 목록으로 표현하고, 외부 exchange calendar 선택은 후속 data quality monitoring 범위로 남긴다.

### 빌드 등급

`cloud-G06`으로 둔다. 변경 파일은 operator 내부로 제한되지만, CLI/YAML/JSONL/text/handoff 계약과 여러 테스트가 함께 맞아야 하며 기존 freshness 완료 근거와 roadmap completion anchor를 유지해야 한다.

## 구현 체크리스트

- [ ] `collection_freshness` scenario schema와 validation에 기대 거래일 및 gap expectation 필드를 추가한다.
- [ ] `runCollectionFreshness`에서 결측 거래일, 중복 일자, provider 지연을 계산하고 expectation mismatch를 검증한다.
- [ ] `StepEvent` JSONL/text 출력과 status fixtures, handoff matrix, handoff tests를 새 key 계약에 맞춘다.
- [ ] `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`를 실행하고 통과 출력을 기록한다.
- [ ] `rg --sort path -n "gap_status|gap_count|gap_yyyymmdd|duplicate_count|duplicate_yyyymmdd|provider_delay_days|expected_yyyymmdd" apps/cli/internal/operator apps/cli/testdata/operator`를 실행해 새 계약 위치를 확인한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [DATASET_GAP-1] Scenario Gap Contract

#### 문제

`apps/cli/internal/operator/scenario.go:251-256`의 `Expect`는 freshness status, missing count, latest date만 표현한다.

```go
// apps/cli/internal/operator/scenario.go:251
FreshnessStatus string `yaml:"freshness_status"`
MissingCount    *int   `yaml:"missing_count"`
LatestYYYYMMDD  string `yaml:"latest_yyyymmdd"`
```

`apps/cli/internal/operator/scenario.go:534-556` validation도 freshness 값과 필수 request window까지만 검사한다.

#### 해결 방법

`Request`에 명시 거래일 목록을 추가하고 `Expect`에 gap 결과 expectation을 추가한다.

```go
// after
ExpectedYYYYMMDD []string `yaml:"expected_yyyymmdd"`

GapStatus         string `yaml:"gap_status"` // clean, gap, duplicate, delayed, mixed, error
GapCount          *int   `yaml:"gap_count"`
DuplicateCount    *int   `yaml:"duplicate_count"`
ProviderDelayDays *int   `yaml:"provider_delay_days"`
```

validation은 daily timeframe의 `collection_freshness`에서 `expected_yyyymmdd`가 `YYYYMMDD`, 오름차순, 중복 없음, request window 안인지 확인한다. `gap_status`는 `clean`, `gap`, `duplicate`, `delayed`, `mixed`, `error`만 허용한다. 명시 거래일이 없으면 gap 계산은 기존 freshness 동작을 유지하고 `gap_status` expectation이 있을 때만 mismatch를 낸다.

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

- [ ] `apps/cli/internal/operator/scenario.go`: request/expect field와 validation 추가.
- [ ] `apps/cli/internal/operator/scenario_test.go`: malformed date, unsorted/duplicate date, unsupported gap status, valid gap contract 테스트 추가.

#### 테스트 작성

`apps/cli/internal/operator/scenario_test.go`에 validation regression tests를 작성한다. assertion 목표는 잘못된 `expected_yyyymmdd`와 `expect.gap_status`가 parse/validation 단계에서 exit code `2` 성격의 오류로 막히는 것이다.

#### 중간 검증

```bash
go test -count=1 ./apps/cli/internal/operator -run 'TestValidate.*(Gap|ExpectedYYYYMMDD|Freshness)'
```

예상: operator package PASS.

### [DATASET_GAP-2] Runner Gap Computation

#### 문제

`apps/cli/internal/operator/runner.go:1145-1268`의 `runCollectionFreshness`는 symbol별 bar 존재 여부와 전체 latest timestamp만 계산한다. bar 날짜별 결측, 중복, 지연을 계산하지 않는다.

```go
// apps/cli/internal/operator/runner.go:1231
ev.MissingSymbols = missingSymbols
ev.MissingCount = len(missingSymbols)

// apps/cli/internal/operator/runner.go:1240
if len(missingSymbols) == 0 {
    ev.FreshnessStatus = "fresh"
} else {
    ev.FreshnessStatus = "missing"
}
```

#### 해결 방법

`collection_freshness`가 bar 조회 중 symbol별 `YYYYMMDD -> count`를 모으게 한다. `request.expected_yyyymmdd`가 있으면 각 expected date가 모든 matched symbol에 존재하는지 검사해 `gap_yyyymmdd`와 `gap_count`를 계산한다. 같은 symbol/date에 bar가 2개 이상이면 `duplicate_yyyymmdd`와 `duplicate_count`를 계산한다. provider delay는 expected list의 마지막 일자와 실제 latest 일자의 expected-list index 차이로 계산한다. latest가 expected list 밖이면 calendar day 차이로 fallback하되 review stub에 실제 선택을 기록한다.

`gap_status`는 다음 우선순위로 산출한다.

```text
error > mixed > gap > duplicate > delayed > clean
```

`mixed`는 gap/duplicate/delayed 중 둘 이상이 동시에 있는 경우다. 기존 `freshness_status`는 symbol-level missing 여부를 유지한다.

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

- [ ] `apps/cli/internal/operator/runner.go`: 날짜 집계 helper, gap status 계산, expectation mismatch 메시지 추가.
- [ ] `apps/cli/internal/operator/runner_market_test.go`: clean, gap, duplicate, delayed, mixed 중 최소 clean/gap/duplicate/delayed 테스트 추가.

#### 테스트 작성

`apps/cli/internal/operator/runner_market_test.go`에 fake API client 기반 regression tests를 작성한다. assertion 목표는 JSONL/text output에 `gap_status`, `gap_count`, `duplicate_count`, `provider_delay_days`, `gap_yyyymmdd`, `duplicate_yyyymmdd`가 의도대로 나타나는 것이다.

#### 중간 검증

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

예상: operator package PASS.

### [DATASET_GAP-3] Output Fixtures And Handoff

#### 문제

`apps/cli/internal/operator/output.go:193-203`과 `output.go:330-340`은 freshness key만 출력한다. `apps/cli/testdata/operator/headless_validation.md:33-34`도 matrix expected keys를 freshness 중심으로 약속한다.

```go
// apps/cli/internal/operator/output.go:193
if ev.FreshnessStatus != "" {
    fields["freshness_status"] = ev.FreshnessStatus
}
```

#### 해결 방법

`StepEvent`에 gap output fields를 추가하고 JSONL/text 렌더링에 포함한다. KR/US status fixtures의 `freshness_status` step에 `expected_yyyymmdd`와 clean gap expectations를 추가하고 expected JSONL에 새 key를 반영한다. handoff matrix와 `handoff_test.go` required key 목록도 같은 key 세트를 약속하게 한다.

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

- [ ] `apps/cli/internal/operator/output.go`: `GapStatus`, `GapCount`, `GapYYYYMMDD`, `DuplicateCount`, `DuplicateYYYYMMDD`, `ProviderDelayDays` 필드와 serialization/text 추가.
- [ ] `apps/cli/testdata/operator/market_data_status_query.yaml`: KR clean gap expectation 추가.
- [ ] `apps/cli/testdata/operator/us_market_data_status_query.yaml`: US clean gap expectation 추가.
- [ ] `apps/cli/testdata/operator/expected/market_data_status_query.jsonl`: expected JSONL 갱신.
- [ ] `apps/cli/testdata/operator/expected/us_market_data_status_query.jsonl`: expected JSONL 갱신.
- [ ] `apps/cli/testdata/operator/headless_validation.md`: matrix expected keys와 checked field 갱신.
- [ ] `apps/cli/internal/operator/handoff_test.go`: required key 목록 갱신.

#### 테스트 작성

기존 fixture/handoff tests를 갱신하고, 새 fixture line에 key가 누락되면 실패하도록 유지한다. 별도 새 package test 파일은 만들지 않는다.

#### 중간 검증

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

예상: operator package PASS.

## 의존 관계 및 구현 순서

이 subtask 디렉터리 이름 `03+02_gap_handling`의 predecessor index는 `02`다. `02`는 `agent-task/archive/2026/06/m-backtest-data-collection-infrastructure/02+01_freshness_status/complete.log`로 완료되었다. 구현 순서는 DATASET_GAP-1, DATASET_GAP-2, DATASET_GAP-3 순서로 진행한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/cli/internal/operator/scenario.go` | DATASET_GAP-1 |
| `apps/cli/internal/operator/scenario_test.go` | DATASET_GAP-1 |
| `apps/cli/internal/operator/runner.go` | DATASET_GAP-2 |
| `apps/cli/internal/operator/runner_market_test.go` | DATASET_GAP-2 |
| `apps/cli/internal/operator/output.go` | DATASET_GAP-3 |
| `apps/cli/internal/operator/handoff_test.go` | DATASET_GAP-3 |
| `apps/cli/testdata/operator/market_data_status_query.yaml` | DATASET_GAP-3 |
| `apps/cli/testdata/operator/us_market_data_status_query.yaml` | DATASET_GAP-3 |
| `apps/cli/testdata/operator/expected/market_data_status_query.jsonl` | DATASET_GAP-3 |
| `apps/cli/testdata/operator/expected/us_market_data_status_query.jsonl` | DATASET_GAP-3 |
| `apps/cli/testdata/operator/headless_validation.md` | DATASET_GAP-3 |

## 최종 검증

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

예상: 두 package 모두 PASS. Go test cache output은 허용하지 않는다. 반드시 `-count=1`로 fresh run을 기록한다.

```bash
rg --sort path -n "gap_status|gap_count|gap_yyyymmdd|duplicate_count|duplicate_yyyymmdd|provider_delay_days|expected_yyyymmdd" apps/cli/internal/operator apps/cli/testdata/operator
```

예상: 새 schema, runner/output, tests, fixtures, handoff matrix 위치가 안정 정렬로 출력된다.

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