<!-- task=m-backtest-multi-timeframe-coverage/02_freshness_readiness plan=0 tag=FRESHNESS -->

# Plan - FRESHNESS

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

이 task는 구현 후 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채워야 완료된다. 검증을 실행하고 active 파일은 그대로 둔 채 리뷰 준비 상태를 보고한다. 종결, archive, `complete.log` 작성은 code-review 전용이다. 선택된 SDD 결정 또는 Milestone `구현 잠금 > 결정 필요` 항목이 막을 때만 review stub의 `사용자 리뷰 요청`을 채우고, 사용자에게 직접 질문하지 않는다.

## 배경

`freshness-readiness` Task는 collection freshness/readiness output이 monthly/daily/minute 상태를 구분해야 한다. 현재 freshness runner는 request timeframe으로 bar를 조회하지만 gap/duplicate/readiness expected list는 daily `expected_yyyymmdd`만 허용한다. 출력도 freshness step의 timeframe을 렌더링하지 않아 여러 timeframe 상태를 한 fixture에 담기 어렵다.

## 사용자 리뷰 요청 흐름

구현 중 직접 사용자 프롬프트를 띄우지 않는다. 선택된 SDD 결정 또는 Milestone lock 결정만 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 active review stub의 `사용자 리뷰 요청`에 기록하고, code-review가 `USER_REVIEW.md` 작성 여부를 판단한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/backtest-loop/milestones/backtest-multi-timeframe-coverage.md`
- Task ids:
  - `freshness-readiness`: collection freshness와 readiness output이 월/일/분 timeframe별 latest/missing/gap/duplicate 상태를 구분한다.
- Completion mode: check-on-pass

## Spec Targets

- SDD: `agent-roadmap/sdd/backtest-loop/backtest-multi-timeframe-coverage/SDD.md`
- Acceptance scenarios:
  - `S08`: task=`freshness-readiness`; evidence=`freshness/readiness fixture text/JSONL for monthly/daily/minute latest/missing/gap/duplicate`
- Completion mode: spec-check-on-pass

## 분석 결과

### 읽은 파일

- `agent-test/local/rules.md`
- `agent-test/local/operations-smoke.md`
- `apps/cli/internal/operator/scenario.go`
- `apps/cli/internal/operator/scenario_test.go`
- `apps/cli/internal/operator/runner.go`
- `apps/cli/internal/operator/runner_market_test.go`
- `apps/cli/internal/operator/output.go`

### 테스트 환경 규칙

- test_env: `local`.
- `agent-test/local/rules.md`를 읽었고 CLI/operator fixture 변경은 local Go test로 검증한다. `operations-smoke`는 bin/go.work 중심이지만 앱 CLI 작업용 별도 profile이 없어 repository Go package test를 fallback으로 사용한다.
- 최종 명령은 `go test -count=1 ./apps/cli/internal/operator`로 둔다.

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

- `apps/cli/internal/operator/scenario.go:763-766`은 `expected_yyyymmdd`를 daily 외 timeframe에서 거부한다.
- `apps/cli/internal/operator/runner.go:1321-1448`은 timestamp를 `YYYYMMDD`로만 bucket 처리한다.
- `apps/cli/internal/operator/output.go:186-190`은 `timeframe`을 matrix output에서만 렌더링하고 freshness output에는 포함하지 않는다.
- committed freshness fixtures는 `market_data_status_query`와 `us_market_data_status_query` daily만 있다.

### 심볼 참조

- rename/remove 없음. 새 YAML field와 StepEvent field를 추가하되 기존 `expected_yyyymmdd`, `latest_yyyymmdd`, `gap_yyyymmdd`는 compatibility로 유지한다.

### 분할 판단

- split policy를 먼저 평가했다.
- shared task group: `m-backtest-multi-timeframe-coverage`.
- sibling plans:
  - `01_fill_policy`: domain/worker backtest 체결 정책.
  - `02_freshness_readiness`: operator freshness/readiness schema/output/fixture.
- 이 plan은 `02_...` 형식이며 `01_...`에 의존하지 않는다. predecessor `complete.log`가 없다.

### 범위 결정 근거

- proto/API contract는 변경하지 않는다. `collection_freshness`는 기존 `ListInstruments`/`ListBars` 조합으로 계산한다.
- provider import 성공 여부나 실제 market calendar는 다루지 않는다. fixture는 fake API bars로 stable evidence를 만든다.
- 기존 daily fixtures는 compatibility evidence로 유지하되, multi-timeframe readiness fixture를 별도로 추가한다.

### 빌드 등급

- `local-G07`: operator schema/runner/output/fixtures를 함께 바꾸지만 앱 CLI package test로 deterministic 검증 가능하다.

## 구현 체크리스트

- [ ] `apps/cli/internal/operator/scenario.go`에 timeframe-aware expected bucket schema를 추가하고 daily `expected_yyyymmdd` compatibility를 유지한다.
- [ ] `apps/cli/internal/operator/runner.go`가 monthly/daily/minute bucket으로 latest/missing/gap/duplicate/provider_delay를 계산하게 만든다.
- [ ] `apps/cli/internal/operator/output.go`가 freshness/readiness step에 `timeframe`과 generic latest/gap/duplicate bucket fields를 text/JSONL로 출력하게 한다.
- [ ] `apps/cli/internal/operator/scenario_test.go`와 `runner_market_test.go`에 monthly/daily/minute_1/minute_5 validation/runner tests를 추가한다.
- [ ] `apps/cli/testdata/operator`에 multi-timeframe freshness/readiness YAML과 expected JSONL fixture를 추가하고 runner/fixture validation tests에 evidence path를 연결한다. 검증: monthly/daily/minute freshness fixture가 stable text/JSONL로 구분된다.
- [ ] 최종 검증 명령을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [FRESHNESS-1] Timeframe-Aware Expected Buckets

#### 문제

`apps/cli/internal/operator/scenario.go:763-766`이 `expected_yyyymmdd`를 daily에만 허용해 monthly/minute gap/duplicate readiness를 표현할 수 없다.

```go
763 if len(step.Request.ExpectedYYYYMMDD) > 0 {
764     if step.Request.Timeframe != "daily" {
765         return fmt.Errorf("step %q: expected_yyyymmdd is only supported for timeframe \"daily\"", step.ID)
```

#### 해결 방법

`Request.ExpectedBuckets []string yaml:"expected_buckets"`와 `Expect.LatestBucket string yaml:"latest_bucket"`을 추가한다. format은 timeframe별로 검증한다: monthly=`YYYYMM`, daily=`YYYYMMDD`, minute_1/minute_5=`YYYYMMDDHHMM`. daily `expected_yyyymmdd`는 기존 fixtures용으로 유지하고 내부에서 daily expected buckets로 normalize한다.

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

- `apps/cli/internal/operator/scenario.go`: schema field, validation, helper 추가.
- `apps/cli/internal/operator/scenario_test.go`: monthly/minute valid/invalid/duplicate/out-of-window tests 추가.

#### 테스트 작성

- 작성: `TestValidateExpectedBuckets`.

#### 중간 검증

```bash
go test -count=1 ./apps/cli/internal/operator -run 'TestValidateExpected(Buckets|YYYYMMDD)'
```

기대: exit 0.

### [FRESHNESS-2] Bucketed Freshness Runner

#### 문제

`apps/cli/internal/operator/runner.go:1321-1448`은 모든 bar timestamp를 `YYYYMMDD`로 집계한다. minute duplicate와 monthly gap을 같은 날짜 단위로 뭉개므로 S08을 증명할 수 없다.

```go
1322 t := time.UnixMilli(bar.GetTimestampUnixMs()).UTC()
1323 d := t.Format("20060102")
1324 dateCounts[d]++
```

#### 해결 방법

runner에 `freshnessBucket(tf, unixMs)`와 normalized expected buckets를 둔다. latest bucket은 max timestamp의 bucket으로 계산하고, gap/duplicate/provider delay는 bucket index 기준으로 계산한다. daily compatibility output은 기존 `latest_yyyymmdd`/`gap_yyyymmdd`도 계속 채운다.

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

- `apps/cli/internal/operator/runner.go`: bucket map 계산, expectation comparison 추가.
- `apps/cli/internal/operator/runner_market_test.go`: monthly gap, minute duplicate, minute delay tests 추가.

#### 테스트 작성

- 작성: `TestRunCollectionFreshnessBucketsByTimeframe`.

#### 중간 검증

```bash
go test -count=1 ./apps/cli/internal/operator -run 'TestRunCollectionFreshness(BucketsByTimeframe|ZeroBarRegression)'
```

기대: exit 0.

### [FRESHNESS-3] Stable Output And Fixture Evidence

#### 문제

`apps/cli/internal/operator/output.go:186-190`은 `timeframe`을 matrix events에만 출력하고, `output.go:233-255`의 freshness block에는 timeframe/generic bucket keys가 없다. Existing freshness fixtures are daily only.

```go
186 if ev.MatrixRunID != "" {
189     fields["timeframe"] = ev.Timeframe
```

#### 해결 방법

freshness events에 `timeframe`, `latest_bucket`, `gap_buckets`, `duplicate_buckets`를 text/JSONL로 출력한다. 새 fixture `multi_timeframe_freshness_readiness.yaml`와 `expected/multi_timeframe_freshness_readiness.jsonl`을 추가해 monthly, daily, minute_1, minute_5 lines를 모두 포함한다. `runner_market_test.go`가 새 fixture path와 JSONL field evidence를 검증하게 한다.

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

- `apps/cli/internal/operator/output.go`: StepEvent fields와 text/JSONL rendering.
- `apps/cli/testdata/operator/multi_timeframe_freshness_readiness.yaml`: 새 fixture.
- `apps/cli/testdata/operator/expected/multi_timeframe_freshness_readiness.jsonl`: 새 expected output.
- `apps/cli/internal/operator/runner_market_test.go`: 새 fixture JSONL validity/path 검증.

#### 테스트 작성

- 작성: `TestMultiTimeframeFreshnessReadinessFixture`.

#### 중간 검증

```bash
go test -count=1 ./apps/cli/internal/operator -run 'Test(MultiTimeframeFreshnessReadinessFixture|ExpectedOutputFixturesAreValidJSONLines)'
```

기대: exit 0.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `apps/cli/internal/operator/scenario.go` | FRESHNESS-1 |
| `apps/cli/internal/operator/scenario_test.go` | FRESHNESS-1 |
| `apps/cli/internal/operator/runner.go` | FRESHNESS-2 |
| `apps/cli/internal/operator/runner_market_test.go` | FRESHNESS-2, FRESHNESS-3 |
| `apps/cli/internal/operator/output.go` | FRESHNESS-3 |
| `apps/cli/testdata/operator/*.yaml` | FRESHNESS-3 |
| `apps/cli/testdata/operator/expected/*.jsonl` | FRESHNESS-3 |

## 최종 검증

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

기대: exit 0, new multi-timeframe freshness/readiness fixture and output tests pass.

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