<!-- task=m-backtest-data-collection-infrastructure/02+01_freshness_status plan=0 tag=CLI -->

# Plan - CLI Freshness Status

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성은 필수다. 구현 전 `agent-task/m-backtest-data-collection-infrastructure/01_watchlist_config/complete.log` 또는 archive의 같은 predecessor complete log가 있는지 확인한다. 구현 후 검증을 실행하고 실제 stdout/stderr를 붙인 뒤 active 파일을 그대로 둔 채 리뷰 준비 상태로 보고한다. 종결, log rename, `complete.log`, archive 이동은 code-review 전용이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 범위 충돌이 필요하면 review stub의 `사용자 리뷰 요청` 섹션에 근거를 기록하고 멈춘다. 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다.

## 배경

`daily-collect`는 실행/재실행 경계가 준비되었지만 operator가 선택 종목군의 성공, 실패, 누락, 최신 일자를 한 번에 확인하는 출력은 아직 없다. 이 작업은 named universe 설정을 전제로 기존 API의 `ListInstruments`와 `ListBars` 응답을 CLI runner에서 조합해 headless freshness 상태 이벤트를 만든다. 새 protobuf/API/worker storage 변경은 이 plan 범위가 아니다.

## 사용자 리뷰 요청 흐름

구현 중 사용자 전용 차단이 생기면 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-data-collection-infrastructure.md`
- Task ids:
  - `freshness-status`: 수집 성공, 실패, 누락, 최신 일자를 operator가 확인할 수 있는 상태 출력을 만든다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/backtest-loop/PHASE.md`
- `agent-roadmap/phase/backtest-loop/milestones/backtest-data-collection-infrastructure.md`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/project/domain/operations/rules.md`
- `agent-ops/rules/project/domain/domain-model/rules.md`
- `agent-ops/rules/project/domain/worker/rules.md`
- `agent-ops/rules/project/domain/api/rules.md`
- `agent-ops/rules/project/domain/contracts/rules.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-test/local/api-smoke.md`
- `agent-test/local/contracts-smoke.md`
- `apps/cli/go.mod`
- `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/client.go`
- `apps/cli/internal/operator/client_test.go`
- `apps/cli/internal/operator/output.go`
- `apps/cli/internal/operator/parser_map.go`
- `apps/cli/internal/operator/parser_map_test.go`
- `apps/cli/internal/cli/cli.go`
- `apps/cli/internal/cli/cli_test.go`
- `packages/contracts/proto/alt/v1/market.proto`
- `services/worker/internal/socket/market_mapping.go`
- `services/worker/internal/socket/market_mapping_test.go`
- `services/worker/internal/storage/ports.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`를 읽었다. 실제 source 변경은 `apps/cli/**`라 `operations-smoke`를 적용한다.
- 읽은 profile: `agent-test/local/operations-smoke.md`. 관련 경계 검토를 위해 domain/worker/api/contracts smoke도 읽었지만, 이 plan은 worker/API/contracts를 수정하지 않는다.
- 적용 명령: `go test ./apps/cli/internal/operator ./apps/cli/internal/cli`.
- `<확인 필요>` 값 없음. 새 외부 dependency 없음.

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

- 기존 `runner_market_test.go`는 import, list instruments, list bars를 개별/순서로 검증하지만 universe별 missing/latest 상태 계산은 덮지 않는다.
- 기존 `output.go`는 import count와 generic count만 출력하고 `freshness_status`, `latest_yyyymmdd`, `missing_count`, `missing_symbols` 같은 operator-facing 상태 키를 출력하지 않는다.
- 이 plan에서 freshness action success, missing symbol, typed error propagation, JSONL/text rendering 테스트를 추가한다.

### 심볼 참조

- renamed/removed symbol 없음. 새 action, request/expect fields, output fields, helper functions만 추가한다.

### 분할 판단

- split 정책을 먼저 평가했다. 공유 task group은 `agent-task/m-backtest-data-collection-infrastructure/`다.
- `01_watchlist_config`: predecessor이며 named universe 설정을 제공한다.
- `02+01_freshness_status`: 이 plan. directory name상 predecessor index `01`에 의존한다.
- predecessor 확인: active `agent-task/m-backtest-data-collection-infrastructure/01_watchlist_config/complete.log` 없음. archive 후보도 없음. 구현 시작 전 `01`이 PASS되어 complete log가 생겨야 한다.
- 이 plan은 CLI runner/output 변경만 다룬다. storage migration이나 protobuf 추가는 별도 split gate로 보류한다.

### 범위 결정 근거

- 새 protobuf/API/worker endpoint를 만들지 않는다. `packages/contracts/proto/alt/v1/market.proto:9`의 `Instrument.provider_symbols`와 `market.proto:29`의 `Bar.timestamp_unix_ms`로 충분히 latest/missing 상태를 계산할 수 있다.
- 실패 상태는 existing typed `ErrorInfo`를 CLI output에 표면화한다. durable import journal이나 retry scheduler는 별도 Milestone/Task 후보로 남긴다.
- Flutter 화면, provider live credentials, DB migration은 범위 밖이다.

### 빌드 등급

- `local-G06`: CLI runner에 다단계 API read orchestration과 output contract가 추가되지만, protocol/storage 변경 없이 focused Go tests로 검증 가능하다.

## 의존 관계 및 구현 순서

- 이 plan의 directory `02+01_freshness_status`는 predecessor `01_watchlist_config` 완료에 의존한다.
- 구현 시작 전 active 또는 archived `m-backtest-data-collection-infrastructure/01_watchlist_config/complete.log`를 확인한다.
- predecessor가 없으면 구현하지 말고 review stub의 `사용자 리뷰 요청`이 아니라 "predecessor missing" 진행 불가 상태로 보고한다.

## 구현 체크리스트

- [ ] `collection_freshness` scenario action과 validation을 추가한다.
- [ ] Runner가 named universe를 기준으로 instruments/bars를 조회해 success/error/missing/latest 상태를 계산한다.
- [ ] Text/JSONL output과 fixtures/headless handoff 문서에 freshness 상태 키를 반영한다.
- [ ] `go test ./apps/cli/internal/operator ./apps/cli/internal/cli`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [CLI-1] Freshness Scenario Action

#### 문제

`apps/cli/internal/operator/scenario.go:23`의 action set에는 import/read/backtest/paper/live actions만 있고 freshness 상태 action이 없다. `apps/cli/internal/operator/scenario.go:432`의 `list_bars` validation은 단일 instrument만 다룬다.

```go
// apps/cli/internal/operator/scenario.go:23
const (
    // ActionHello validates a basic API connection handshake.
    ActionHello Action = "hello"
    // ActionImportDailyBars imports market data daily bars.
    ActionImportDailyBars Action = "import_daily_bars"
```

```go
// apps/cli/internal/operator/scenario.go:432
case ActionListBars:
    if step.Request.InstrumentID == "" {
        return fmt.Errorf("step %q: list_bars requires request.instrument_id", step.ID)
    }
```

#### 해결 방법

새 action 이름은 `collection_freshness`로 둔다. request는 `universe`, `timeframe`, `from_unix_ms`, `to_unix_ms`를 요구한다. `expect`에는 `freshness_status`, `missing_count`, `latest_yyyymmdd`를 선택적으로 추가한다. `watchlist-config`가 materialize한 universe를 기준으로 provider/market/symbols를 사용할 수 있어야 한다.

```go
const (
    ActionCollectionFreshness Action = "collection_freshness"
)

type Expect struct {
    ...
    FreshnessStatus string `yaml:"freshness_status"`
    MissingCount    *int   `yaml:"missing_count"`
    LatestYYYYMMDD  string `yaml:"latest_yyyymmdd"`
}
```

Validation은 `request.universe`, supported timeframe, non-zero ordered window를 확인하고, `expect.freshness_status`는 `fresh`, `missing`, `error` 중 하나만 허용한다.

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

- [ ] `apps/cli/internal/operator/scenario.go`: action constant/map, request/expect fields, validation 추가.
- [ ] `apps/cli/internal/operator/scenario_test.go`: missing universe/timeframe/window, invalid expect status, valid freshness scenario 테스트 추가.

#### 테스트 작성

- `TestValidateCollectionFreshness`와 invalid matrix cases 추가.

#### 중간 검증

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

기대 결과: exit code 0.

### [CLI-2] Freshness Runner And Output

#### 문제

`apps/cli/internal/operator/runner.go:218`의 `runStep` switch는 `list_instruments`와 `list_bars`를 개별 step으로만 처리한다. `apps/cli/internal/operator/output.go:23`의 `StepEvent`에는 import counts는 있지만 freshness-specific fields가 없다.

```go
// apps/cli/internal/operator/runner.go:231
case ActionListInstruments:
    resp, err := client.ListInstruments(stepCtx, &altv1.ListInstrumentsRequest{
        Market: marketByName[step.Request.Market],
    })
```

```go
// apps/cli/internal/operator/output.go:41
// Import-specific output fields
Provider        string
InstrumentCount int
BarCount        int
```

#### 해결 방법

`runCollectionFreshness` helper를 추가한다. 흐름은 다음 순서다.

1. `ListInstruments(market, provider)`를 호출한다.
2. universe symbols를 provider symbol 또는 instrument symbol로 instrument id에 매칭한다.
3. 매칭된 instrument마다 `ListBars(instrument_id, timeframe, from, to)`를 호출한다.
4. typed error가 오면 `freshness_status=error`와 error code/message를 출력한다.
5. symbol이 없거나 bar가 없으면 missing으로 기록한다.
6. 모든 bar timestamp 중 max를 `latest_yyyymmdd`로 출력한다.
7. missing_count가 0이면 `freshness_status=fresh`, 아니면 `missing`으로 출력한다.

`StepEvent`와 `Writer.WriteStep`에 `Universe`, `FreshnessStatus`, `LatestYYYYMMDD`, `MissingCount`, `MissingSymbols`를 추가한다. Text output은 `missing_symbols=A,B`처럼 comma-separated로, JSONL은 `missing_symbols` array로 출력한다.

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

- [ ] `apps/cli/internal/operator/runner.go`: `ActionCollectionFreshness` case, helper, expectation evaluation 추가.
- [ ] `apps/cli/internal/operator/output.go`: freshness fields와 text/jsonl rendering 추가.
- [ ] `apps/cli/internal/operator/runner_market_test.go`: fresh, missing instrument, empty bars, typed error, call order 테스트 추가. fake API에 per-instrument bars response를 지원한다.

#### 테스트 작성

- `TestRunScenarioCollectionFreshnessFresh`: all symbols present, latest date 출력.
- `TestRunScenarioCollectionFreshnessMissingSymbol`: missing_count/missing_symbols 출력.
- `TestRunScenarioCollectionFreshnessTypedError`: typed ErrorInfo가 `status=error`로 표면화.
- JSONL rendering assertion은 expected fixture 또는 direct writer assertion으로 고정한다.

#### 중간 검증

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

기대 결과: exit code 0.

### [CLI-3] Freshness Fixtures And Handoff

#### 문제

현재 `apps/cli/testdata/operator/market_data_status_query.yaml:29` 이후에는 `list_instruments`와 단일 `list_bars` count만 있어 operator가 누락 symbol과 최신 일자를 한 줄로 볼 수 없다. handoff matrix도 `count` 중심이다.

```yaml
# apps/cli/testdata/operator/market_data_status_query.yaml:29
  - id: list_kr_instruments
    action: list_instruments
...
  - id: list_daily_bars
    action: list_bars
```

#### 해결 방법

KR/US status fixtures에 `collection_freshness` step을 추가한다. expected JSONL에는 `freshness_status`, `latest_yyyymmdd`, `missing_count` 키를 추가한다. `headless_validation.md`의 market data status rows도 expected output keys와 protobuf/view-model field 설명을 갱신한다.

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

- [ ] `apps/cli/testdata/operator/market_data_status_query.yaml`: freshness step 추가.
- [ ] `apps/cli/testdata/operator/us_market_data_status_query.yaml`: freshness step 추가.
- [ ] `apps/cli/testdata/operator/expected/market_data_status_query.jsonl`: expected keys 갱신.
- [ ] `apps/cli/testdata/operator/expected/us_market_data_status_query.jsonl`: expected keys 갱신.
- [ ] `apps/cli/testdata/operator/headless_validation.md`: matrix keys와 field 설명 갱신.

#### 테스트 작성

- Existing fixture loading/running tests가 expected freshness output을 확인하도록 갱신한다.

#### 중간 검증

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

기대 결과: exit code 0.

## 수정 파일 요약

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

## 최종 검증

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

기대 결과: exit code 0. Go test cache output은 허용한다. 모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
