<!-- task=m-scheduled-market-data-refresh/02_retry_backfill plan=0 tag=BACKFILL -->

# Plan - BACKFILL

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

`CODE_REVIEW-local-G04.md`의 구현 에이전트 소유 섹션을 채우는 것이 구현의 마지막 단계다. 검증 명령을 실행하고 실제 stdout/stderr를 붙여 넣은 뒤 active 파일을 그대로 두고 review ready 상태로 보고한다. 선택된 SDD 결정이나 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 막으면 직접 질문하지 말고 review stub의 `사용자 리뷰 요청` 섹션에 근거를 기록한다. 구현 에이전트는 `USER_REVIEW.md`, `complete.log`, archive 로그를 만들지 않는다.

## 배경

`retry-backfill`은 provider delay, missing, gap을 성공으로 숨기지 않고 stale/error 상태와 backfill window로 남겨야 한다. 현 코드에는 schedule config validation은 있지만 freshness result를 retry/backfill decision으로 바꾸는 worker-local 계산 표면이 없다. SDD S03은 fixture 또는 local smoke에서 missing/gap/provider delay 케이스가 stable text/JSONL로 구분되는 증거를 요구한다.

## 사용자 리뷰 요청 흐름

사용자 리뷰는 선택된 SDD decision 또는 Milestone lock decision만 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 중 직접 채팅 질문, 선택지 제시, `request_user_input` 호출은 금지한다. code-review가 요청 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/backtest-loop/milestones/scheduled-market-data-refresh.md`
- Task ids:
  - `retry-backfill`: provider 지연, 결측, gap 결과에 따라 retry와 backfill window를 계산하고 실패를 stale/error 상태로 남긴다.
- Completion mode: check-on-pass

## Spec Targets

- SDD: `agent-roadmap/sdd/backtest-loop/scheduled-market-data-refresh/SDD.md`
- Acceptance scenarios:
  - `S03`: task=`retry-backfill`; evidence=`missing/gap/provider delay fixture 또는 local smoke`
- Completion mode: spec-check-on-pass

## 분석 결과

### 읽은 파일

- `agent-test/local/rules.md`
- `agent-test/local/worker-smoke.md`
- `agent-roadmap/phase/backtest-loop/milestones/scheduled-market-data-refresh.md`
- `agent-roadmap/sdd/backtest-loop/scheduled-market-data-refresh/SDD.md`
- `services/worker/internal/scheduler/config.go`
- `services/worker/internal/scheduler/config_test.go`
- `services/worker/internal/jobs/marketdata_jobs.go`
- `services/worker/internal/marketdata/importer/importer.go`
- `services/worker/internal/storage/postgres/queries/queries.sql`
- `services/worker/go.mod`

### 테스트 환경 규칙

test_env는 `local`이다. `agent-test/local/rules.md`와 `agent-test/local/worker-smoke.md`를 읽었다. `services/worker/**` 변경이므로 focused command는 `go test -count=1 ./services/worker/internal/scheduler`, 전체 worker smoke는 `go test ./services/worker/...`다. storage SQL을 바꾸지 않으므로 `bin/worker-storage-check`는 필수 검증이 아니다.

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

- schedule validation output은 현재 커버된다.
- missing/gap/provider_delay를 backfill decision으로 바꾸는 worker-local tests가 없다.
- stale/error text/JSONL output fixture가 없다.

### 심볼 참조

none. symbol rename/remove 없음.

### 분할 판단

split decision policy를 먼저 평가했다. shared task group은 `m-scheduled-market-data-refresh`다. 이 subtask는 `02_retry_backfill`이고 `depends_on=[]`이다. `scheduled-runner`와 독립적으로 pure calculator와 stable output을 만들 수 있어 predecessor `complete.log`를 요구하지 않는다.

### 범위 결정 근거

이 plan은 retry/backfill calculation과 stable evidence output만 다룬다. worker long-running loop, API/contracts schema, status CLI, remote runner smoke, backtest-readiness는 제외한다. provider live 호출과 secret 값은 다루지 않는다.

### 빌드 등급

local-G04. 새 계산 표면과 fixture tests가 필요하지만 worker 내부 pure logic이며 외부 runtime 없이 검증 가능하다.

## 구현 체크리스트

- [ ] `services/worker/internal/scheduler`에 freshness observation -> retry/backfill decision 계산 타입과 함수를 추가한다.
- [ ] missing/gap/provider_delay/error 케이스를 stable text/JSONL로 출력하는 writer와 fixture-style unit tests를 추가한다.
- [ ] `retry-backfill` 검증: fixture 또는 local smoke에서 missing/gap/provider delay 케이스가 stable text/JSONL로 구분된다.
- [ ] 최종 검증으로 `go test -count=1 ./services/worker/internal/scheduler`와 `go test ./services/worker/...`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [BACKFILL-1] Retry/Backfill Decision Calculator

문제: SDD는 provider delay나 gap을 성공으로 숨기지 말라고 고정한다(`agent-roadmap/sdd/backtest-loop/scheduled-market-data-refresh/SDD.md:75`). 현재 scheduler package는 validation item만 정의하고(`services/worker/internal/scheduler/config.go:74`) freshness observation이나 backfill decision type이 없다.

Before:

```go
// services/worker/internal/scheduler/config.go:74
// ValidationItem is a stable text/JSONL evidence row.
type ValidationItem struct {
```

해결 방법: `services/worker/internal/scheduler/backfill.go`를 추가한다. `FreshnessObservation`은 schedule, provider, selector, timeframe, expected_from/to, latest, missing dates, gap buckets, duplicate count, provider_delay_days, error를 담는다. `DecideBackfill(observation, window)`는 success/fresh, stale/missing, stale/gap, stale/provider_delay, error를 분리하고 earliest missing/gap/latest 기준으로 inclusive `backfill_from`/`backfill_to`를 계산한다.

수정 파일 및 체크리스트:

- [ ] `services/worker/internal/scheduler/backfill.go`: `FreshnessObservation`, `BackfillDecision`, `DecideBackfill`.
- [ ] `services/worker/internal/scheduler/backfill_test.go`: missing, gap, provider delay, duplicate-only, error 케이스.

테스트 작성: `TestDecideBackfillDistinguishesMissingGapProviderDelay`와 `TestDecideBackfillPreservesErrorState`를 추가한다.

중간 검증:

```bash
go test -count=1 ./services/worker/internal/scheduler
```

기대 결과: exit code 0. backfill calculator tests pass.

### [BACKFILL-2] Stable Text/JSONL Evidence

문제: SDD S03은 stale/error 상태와 backfill window가 stable text/JSONL로 구분되어야 한다(`agent-roadmap/sdd/backtest-loop/scheduled-market-data-refresh/SDD.md:87`). 현재 scheduler output은 config validation용 `WriteText`/`WriteJSONL`뿐이고(`services/worker/internal/scheduler/config.go:272`), retry/backfill decision writer가 없다.

Before:

```go
// services/worker/internal/scheduler/config.go:272
// WriteText renders stable operator-facing dry-run lines.
func WriteText(w io.Writer, validation Validation) error {
```

해결 방법: `WriteBackfillText`와 `WriteBackfillJSONL`을 추가한다. text는 `schedule=<name> status=<fresh|stale|error> reason=<...> backfill_from=<YYYYMMDD> backfill_to=<YYYYMMDD> retry=<true|false>` 형태로, JSONL은 같은 필드를 stable struct tag로 출력한다. tests는 expected substrings와 JSONL fields를 모두 고정한다.

수정 파일 및 체크리스트:

- [ ] `services/worker/internal/scheduler/backfill.go`: writer 함수와 JSON field tags.
- [ ] `services/worker/internal/scheduler/backfill_test.go`: text/JSONL expected output tests.

테스트 작성: `TestWriteBackfillTextAndJSONL`를 추가한다.

중간 검증:

```bash
go test -count=1 ./services/worker/internal/scheduler
```

기대 결과: exit code 0. stable output tests pass.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/worker/internal/scheduler/backfill.go` | BACKFILL-1, BACKFILL-2 |
| `services/worker/internal/scheduler/backfill_test.go` | BACKFILL-1, BACKFILL-2 |

## 최종 검증

```bash
go test -count=1 ./services/worker/internal/scheduler
go test ./services/worker/...
```

Go test cache output is acceptable only for the broad second command; the focused first command must use `-count=1`.

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