<!-- task=m-persistence-worker-backbone/02+01_worker_runtime plan=0 tag=WORKER -->

# Plan - WORKER

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이 구현의 마지막 필수 단계다. 구현 후 검증 명령을 실행하고 실제 출력과 구현 메모를 기록한 뒤 active 파일을 그대로 둔 채 리뷰 준비를 보고한다. 사용자만 결정할 수 있는 범위 변경, 외부 환경 준비, 또는 계획 충돌로 막히면 active review stub의 `사용자 리뷰 요청` 섹션에 정확한 증거와 재개 조건을 적고 멈춘다. 구현 에이전트는 `USER_REVIEW.md`, archive log, `complete.log`를 만들지 않는다.

## 배경

storage foundation이 완료되면 worker는 Redis key boundary와 기본 job execution model을 가져야 한다. 현재 worker entrypoint는 config 값을 읽고 로그만 남기므로, 비동기 작업을 표현하거나 실행할 수 없다. 이 plan은 API 내부 구현에 의존하지 않는 worker-owned runtime surface를 만든다.

## 사용자 리뷰 요청 흐름

구현 중 사용자 결정이나 외부 환경 준비가 필요하면 active `CODE_REVIEW-*-G??.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`에서 복사된 형식이며, code-review가 검증 후 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/foundation-alignment/PHASE.md`
- `agent-roadmap/phase/foundation-alignment/milestones/persistence-worker-backbone.md`
- `agent-ops/rules/project/domain/worker/rules.md`
- `agent-ops/rules/project/domain/operations/rules.md`
- `agent-ops/rules/private/testing-env.md`
- `services/worker/go.mod`
- `services/worker/internal/config/config.go`
- `services/worker/cmd/alt-worker/main.go`
- `deployments/local/docker-compose.yml`
- `packages/domain/market/types.go`
- `packages/domain/backtest/types.go`
- `bin/infra-check`
- `bin/dev`
- `bin/test`
- `bin/lint`
- `bin/build`

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

- Redis key prefix/config: 기존 config test 없음. fallback/env override test를 추가한다.
- Job model/executor: 기존 package 없음. handler dispatch, unknown kind, context cancellation test를 추가한다.
- API independence: 기존 compile boundary만 있다. `rg --sort path`로 `services/api/internal` import가 없는지 검증한다.
- Live Redis: default test에는 Redis 접속을 넣지 않는다. local compose health는 `bin/infra-check`로 별도 확인한다.

### 심볼 참조

- Renamed/removed symbols: none.
- `config.Config`는 필드 추가만 한다. 기존 call site는 `services/worker/cmd/alt-worker/main.go:10-11` 하나다.

### 분할 판단

- split decision policy를 계획 파일 선택 전에 평가했다.
- shared task group: `m-persistence-worker-backbone`
- `01_storage_foundation`: migration, generated queries, storage ports를 먼저 완료해야 한다.
- `02+01_worker_runtime`: 이 subtask는 directory name에 따라 `01_storage_foundation`의 `complete.log`가 선행 조건이다.
- Redis/job runtime은 storage schema와 별도 위험 프로파일이므로 독립 리뷰 단위로 둔다.

### 범위 결정 근거

- 실제 market data import, KIS 호출, backtest engine 실행은 이 Milestone의 범위 제외라 구현하지 않는다.
- API enqueue endpoint나 client UI는 operator/client milestone 영역이라 제외한다.
- Redis를 full durable queue로 확정하지 않는다. 이 작업은 key prefix, purpose, adapter boundary, basic executor까지만 다룬다.
- `services/worker/README.md`가 필요하면 worker-local 운영 설명만 담고, tracked top-level docs나 private 환경값은 확장하지 않는다.

### 빌드 등급

- build lane: `cloud-G07`, review lane: `cloud-G07`.
- Redis boundary, worker execution model, cross-domain import boundary를 다루며 storage subtask에 의존하므로 higher review fidelity가 필요하다.

## 구현 체크리스트

- [ ] [WORKER-1] Redis 사용 목적과 key prefix 기준을 worker config 또는 worker-local 문서에 정리한다.
- [ ] [WORKER-2] API 내부 구현에 의존하지 않는 worker job model과 executor scaffold를 추가한다. 검증: worker job이 API 내부 구현에 의존하지 않는다.
- [ ] `services/worker` fresh test와 workspace 회귀 검증을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

`02+01_worker_runtime`은 같은 task group의 `01_storage_foundation`에 의존한다. 구현 시작 전 `agent-task/m-persistence-worker-backbone/01_storage_foundation/complete.log`가 존재해야 한다. directory name에 없는 추가 선행 조건은 없다.

### [WORKER-1] Redis Boundary and Config

#### 문제

`services/worker/internal/config/config.go:5-14`는 `DatabaseURL`, `RedisURL`만 제공한다. Milestone은 `agent-roadmap/phase/foundation-alignment/milestones/persistence-worker-backbone.md:37`에서 Redis 사용 목적과 key prefix 기준을 요구하지만, 현재 config나 worker-local 문서에 prefix 기준이 없다.

Before (`services/worker/internal/config/config.go:5-14`):

```go
type Config struct {
	DatabaseURL string
	RedisURL    string
}

func Load() Config {
	return Config{
		DatabaseURL: getenv("DATABASE_URL", "postgres://alt:alt@localhost:5432/alt?sslmode=disable"),
		RedisURL:    getenv("REDIS_URL", "redis://localhost:6379/0"),
	}
}
```

After:

```go
type Config struct {
	DatabaseURL    string
	RedisURL       string
	RedisKeyPrefix string
	WorkerQueue    string
}

func Load() Config {
	return Config{
		DatabaseURL:    getenv("DATABASE_URL", "postgres://alt:alt@localhost:5432/alt?sslmode=disable"),
		RedisURL:       getenv("REDIS_URL", "redis://localhost:6379/0"),
		RedisKeyPrefix: getenv("ALT_REDIS_KEY_PREFIX", "alt"),
		WorkerQueue:    getenv("ALT_WORKER_QUEUE", "default"),
	}
}
```

#### 해결 방법

- `Config`에 `RedisKeyPrefix`와 `WorkerQueue`를 추가한다.
- `services/worker/internal/rediskeys` package를 추가해 Redis key가 `<prefix>:worker:<purpose>:<id>` 형태로만 만들어지게 한다.
- worker-local README 또는 config comments 중 하나에 Redis purpose를 기록한다: coordination/cache/queue surface, not source of truth.
- `main.go` log에는 secret-bearing URL 전체를 출력하지 않고 prefix/queue처럼 안전한 값만 기록한다.

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

- [ ] `services/worker/internal/config/config.go`: prefix/queue env fallback을 추가한다.
- [ ] `services/worker/internal/config/config_test.go`: fallback과 env override test를 추가한다.
- [ ] `services/worker/internal/rediskeys/keys.go`: key builder를 추가한다.
- [ ] `services/worker/internal/rediskeys/keys_test.go`: prefix normalization과 empty id rejection을 검증한다.
- [ ] `services/worker/README.md` 또는 config package doc: Redis purpose/key prefix 기준을 worker-local로 기록한다.
- [ ] `services/worker/cmd/alt-worker/main.go`: safe runtime metadata log를 갱신한다.

#### 테스트 작성

- 작성: `services/worker/internal/config/config_test.go`, `services/worker/internal/rediskeys/keys_test.go`
- 테스트명: `TestLoadDefaults`, `TestLoadEnvOverrides`, `TestWorkerKey`, `TestWorkerKeyRejectsEmptyID`
- 목표: env fallback, explicit override, key prefix formatting을 검증한다.

#### 중간 검증

```bash
(cd services/worker && go test -count=1 ./internal/config ./internal/rediskeys)
```

예상 결과: config/redis key package가 fresh test로 통과한다.

### [WORKER-2] Job Model and Executor

#### 문제

`services/worker/cmd/alt-worker/main.go:9-11`은 worker process가 실행할 job model을 만들지 않는다. worker domain rule은 `agent-ops/rules/project/domain/worker/rules.md:34-35`에서 job orchestration을 worker 내부에 두고 shared domain type을 쓰라고 요구하며, Milestone은 `agent-roadmap/phase/foundation-alignment/milestones/persistence-worker-backbone.md:38`에서 API 내부 구현에 의존하지 않는 job model을 요구한다.

Before (`services/worker/cmd/alt-worker/main.go:9-11`):

```go
func main() {
	cfg := config.Load()
	slog.Info("worker scaffold ready", "database_url_set", cfg.DatabaseURL != "", "redis_url_set", cfg.RedisURL != "")
}
```

After:

```go
func main() {
	cfg := config.Load()
	runner := jobs.NewRunner()
	jobs.RegisterBuiltins(runner)
	slog.Info("worker ready", "redis_key_prefix", cfg.RedisKeyPrefix, "worker_queue", cfg.WorkerQueue, "handlers", runner.Len())
}
```

#### 해결 방법

- `services/worker/internal/jobs`에 `Kind`, `Job`, `Handler`, `Runner`를 추가한다.
- Built-in kind는 import daily bars, normalize daily bars, run backtest 정도의 placeholder로 둔다. 실제 KIS/backtest engine 호출은 하지 않는다.
- handler payload는 `json.RawMessage` 또는 typed request struct로 시작하되, API 내부 package import는 금지한다.
- storage foundation의 ports가 필요하면 interface dependency만 받는다. concrete Postgres adapter construction은 main bootstrap 또는 later task로 남긴다.

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

- [ ] `services/worker/internal/jobs/job.go`: job kind/id/status/payload shape를 추가한다.
- [ ] `services/worker/internal/jobs/runner.go`: handler registration과 execution을 추가한다.
- [ ] `services/worker/internal/jobs/builtin.go`: built-in kinds와 no-op placeholder handlers를 추가한다.
- [ ] `services/worker/internal/jobs/*_test.go`: dispatch, unknown kind, context cancellation test를 추가한다.
- [ ] `services/worker/cmd/alt-worker/main.go`: config와 runner bootstrap을 연결한다.
- [ ] `services/worker/go.mod`: 필요한 dependency가 있으면 추가하되 API module dependency는 추가하지 않는다.

#### 테스트 작성

- 작성: `services/worker/internal/jobs/runner_test.go`
- 테스트명: `TestRunnerDispatchesRegisteredHandler`, `TestRunnerRejectsUnknownKind`, `TestRunnerRespectsCanceledContext`
- 목표: basic job execution model이 deterministic하게 동작하고 API 내부 구현에 의존하지 않음을 compile/test로 검증한다.

#### 중간 검증

```bash
(cd services/worker && go test -count=1 ./internal/jobs)
```

예상 결과: job runner package가 fresh test로 통과한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/worker/internal/config/config.go` | WORKER-1 |
| `services/worker/internal/config/config_test.go` | WORKER-1 |
| `services/worker/internal/rediskeys/keys.go` | WORKER-1 |
| `services/worker/internal/rediskeys/keys_test.go` | WORKER-1 |
| `services/worker/README.md` | WORKER-1 |
| `services/worker/internal/jobs/job.go` | WORKER-2 |
| `services/worker/internal/jobs/runner.go` | WORKER-2 |
| `services/worker/internal/jobs/builtin.go` | WORKER-2 |
| `services/worker/internal/jobs/*_test.go` | WORKER-2 |
| `services/worker/cmd/alt-worker/main.go` | WORKER-1, WORKER-2 |
| `services/worker/go.mod` | WORKER-2 |

## 최종 검증

Go test cache는 worker package에서는 허용하지 않는다. `go test -count=1`을 사용한다. Root `bin/test`의 Go cache 사용은 fresh worker package 검증 뒤 workspace regression 용도로 허용한다.

```bash
test -f agent-task/m-persistence-worker-backbone/01_storage_foundation/complete.log
```

```bash
command -v docker || test -x /Applications/Docker.app/Contents/Resources/bin/docker
```

```bash
DOCKER="$(command -v docker || printf '%s' /Applications/Docker.app/Contents/Resources/bin/docker)" && "$DOCKER" compose -f deployments/local/docker-compose.yml up -d postgres redis
```

```bash
bin/infra-check
```

```bash
(cd services/worker && go test -count=1 ./...)
```

```bash
! rg --sort path -n 'services/api/internal' services/worker
```

```bash
bin/test
```

```bash
bin/lint
```

```bash
bin/build
```

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