<!-- task=m-persistence-worker-backbone/01_storage_foundation plan=0 tag=STORAGE -->

# Plan - STORAGE

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

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

## 배경

현재 Milestone은 PostgreSQL/Redis를 durable worker backbone의 기준 인프라로 고정했지만, worker는 아직 URL을 읽고 로그만 남기는 scaffold다. `infra-check`로 환경 불일치가 드러났으므로, 다음 단계는 local compose 기준의 migration entrypoint와 type-safe query generation 흐름을 먼저 고정하는 것이다. 이 작업이 끝나야 후속 worker job runtime이 storage port 위에 안전하게 올라갈 수 있다.

## 사용자 리뷰 요청 흐름

구현 중 사용자 결정이나 외부 환경 준비가 필요하면 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`

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

- Migration entrypoint: 기존 테스트 없음. 새 migration runner/order test와 config test를 추가한다.
- Query generation flow: 기존 흐름 없음. `sqlc` 설정과 generated query compile을 `go test -count=1 ./...`로 검증한다.
- Storage port/domain mapping: 기존 adapter 없음. domain type mapping helper test를 새로 작성한다.
- Live local infra: 현재 컨테이너에는 Docker가 없어 직접 통과시킬 수 없다. 구현 검증은 macOS field host 또는 Docker 가능 환경에서 local compose를 올린 뒤 `bin/infra-check`와 migration command로 확인한다.

### 심볼 참조

- Renamed/removed symbols: none.
- 새 public-ish 내부 경계만 추가한다. 기존 call site 변경은 `services/worker/cmd/alt-worker/main.go`와 새 migration command에 한정한다.

### 분할 판단

- split decision policy를 계획 파일 선택 전에 평가했다.
- shared task group: `m-persistence-worker-backbone`
- `01_storage_foundation`: migration, query generation, storage ports/adapters를 먼저 만든다.
- `02+01_worker_runtime`: `01_storage_foundation`의 `complete.log` 이후 job runtime과 Redis boundary를 얹는다.
- storage/migration은 schema와 generated code 위험이 있고, worker runtime은 Redis/job orchestration 위험이 있으므로 별도 리뷰 단위가 필요하다.

### 범위 결정 근거

- `services/api/`는 API socket/session boundary라서 이 작업에서 제외한다.
- Flutter client와 socket smoke는 이미 별도 milestone 완료 검증 대상이므로 제외한다.
- market data import, backtest engine 전체, production deployment는 Milestone 범위 제외에 있으므로 구현하지 않는다.
- `deployments/local/docker-compose.yml`의 Postgres/Redis 기본값은 이미 worker config fallback과 맞으므로, 필요한 경우 health/volume 변경만 한다.

### 빌드 등급

- build lane: `cloud-G07`, review lane: `cloud-G07`.
- storage/migration/schema/query generation이 포함되고 live infra 검증 가능성이 있어 local 단독보다 높은 검증 판단이 필요하다.

## 구현 체크리스트

- [ ] [STORAGE-1] PostgreSQL migration entrypoint와 sqlc query generation 흐름을 추가한다. 검증: local PostgreSQL/Redis를 실행한 뒤 worker가 필요한 설정을 읽고 migration command가 DB에 접속할 수 있음을 확인한다.
- [ ] [STORAGE-2] domain과 persistence 사이의 storage port 및 Postgres adapter 경계를 추가한다.
- [ ] `services/worker` fresh test와 workspace 회귀 검증을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

이 subtask는 선행 subtask가 없다. `02+01_worker_runtime`은 이 디렉터리에 `complete.log`가 생긴 뒤 시작한다.

### [STORAGE-1] Migration and Query Generation

#### 문제

`services/worker/go.mod:1-3`에는 worker module 선언만 있고 DB/query tool dependency가 없다. `services/worker/cmd/alt-worker/main.go:9-11`은 config presence만 로그로 남기며 migration entrypoint가 없다. Milestone의 `[db-migrations]`, `[typed-queries]`는 `agent-roadmap/phase/foundation-alignment/milestones/persistence-worker-backbone.md:35-36`에서 아직 미완료다.

Before (`services/worker/go.mod:1-3`):

```go
module git.toki-labs.com/toki/alt/services/worker

go 1.22
```

After:

```go
module git.toki-labs.com/toki/alt/services/worker

go 1.22

require (
	github.com/jackc/pgx/v5 v5.7.2
	github.com/sqlc-dev/sqlc v1.27.0
)
```

#### 해결 방법

- `services/worker/sqlc.yaml`을 추가하고 engine은 PostgreSQL, Go output은 `internal/storage/postgres/sqlc`로 둔다.
- `services/worker/internal/storage/postgres/migrations/000001_worker_backbone.up.sql`와 `.down.sql`을 추가한다.
- `services/worker/internal/storage/postgres/migrate.go`에서 embedded migration 파일을 파일명 순서대로 적용하고 `schema_migrations` table로 중복 적용을 막는다.
- `services/worker/cmd/alt-worker-migrate/main.go`를 추가해 `config.Load()`의 `DATABASE_URL`로 migration을 실행한다.
- `services/worker/tools.go`에 `//go:build tools` import로 `sqlc` 버전을 고정한다.
- `bin/worker-storage-gen`과 `bin/worker-storage-check`를 추가해 query generation과 generated diff check를 반복 가능하게 한다.

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

- [ ] `services/worker/go.mod`, `services/worker/go.sum`: pgx/sqlc dependencies를 pin한다.
- [ ] `services/worker/tools.go`: sqlc tool dependency를 기록한다.
- [ ] `services/worker/sqlc.yaml`: query generation config를 추가한다.
- [ ] `services/worker/internal/storage/postgres/migrations/*.sql`: worker backbone schema를 추가한다.
- [ ] `services/worker/internal/storage/postgres/queries/*.sql`: sqlc query source를 추가한다.
- [ ] `services/worker/internal/storage/postgres/migrate.go`: embedded migration runner를 추가한다.
- [ ] `services/worker/cmd/alt-worker-migrate/main.go`: migration command entrypoint를 추가한다.
- [ ] `bin/worker-storage-gen`, `bin/worker-storage-check`: root workflow command를 추가하고 실행권한을 부여한다.
- [ ] `bin/test` 또는 `bin/build`: 필요한 경우 generated query check를 workspace 검증에 포함한다.

#### 테스트 작성

- 작성: `services/worker/internal/storage/postgres/migrate_test.go`
- 테스트명: `TestMigrationsAreEmbeddedInOrder`, `TestMigrationStatementsAreNamed`
- 목표: migration 파일명이 정렬 가능하고 up/down pair가 빠지지 않으며 migration runner가 빈 목록을 허용하지 않음을 검증한다.
- live DB test는 기본 `bin/test`에 넣지 않는다. Docker 없는 기본 환경에서도 workspace test가 통과해야 하기 때문이다.

#### 중간 검증

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

예상 결과: migration/config 관련 package가 fresh test로 통과한다.

### [STORAGE-2] Storage Ports and Postgres Adapter

#### 문제

domain model은 `packages/domain/market/types.go:35-69`와 `packages/domain/backtest/types.go:9-41`에 있지만, persistence boundary가 없어 worker code가 DB schema 또는 API 내부에 직접 붙을 위험이 있다. worker domain rule은 `agent-ops/rules/project/domain/worker/rules.md:33-35`에서 worker-specific adapter와 shared domain type 사용을 요구한다.

Before (`packages/domain/backtest/types.go:30-36`):

```go
type Run struct {
	ID        RunID
	Spec      RunSpec
	Status    RunStatus
	CreatedAt time.Time
	UpdatedAt time.Time
}
```

After:

```go
// domain type remains unchanged; storage ports map to this shape.
type RunStore interface {
	UpsertRun(ctx context.Context, run backtest.Run) error
	GetRun(ctx context.Context, id backtest.RunID) (backtest.Run, error)
}
```

#### 해결 방법

- `services/worker/internal/storage/ports.go`에 `InstrumentStore`, `BarStore`, `BacktestRunStore`를 정의한다.
- `services/worker/internal/storage/postgres`에 sqlc generated query를 감싼 adapter를 둔다.
- domain conversion helper를 adapter 내부에 두고 `market.Decimal.Value`는 DB numeric/text 변환에서 명시적으로 실패를 반환한다.
- API internals import를 금지하고 `packages/domain/market`, `packages/domain/backtest`만 business shape로 사용한다.

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

- [ ] `services/worker/internal/storage/ports.go`: domain-facing interfaces를 추가한다.
- [ ] `services/worker/internal/storage/postgres/store.go`: pgxpool 기반 store constructor와 adapter methods를 추가한다.
- [ ] `services/worker/internal/storage/postgres/mapping.go`: domain/DB conversion helper를 추가한다.
- [ ] `services/worker/internal/storage/postgres/sqlc/*.go`: generated query code를 commit한다.
- [ ] `services/worker/internal/storage/postgres/mapping_test.go`: decimal/status/time mapping test를 추가한다.
- [ ] `services/worker/cmd/alt-worker/main.go`: 필요한 경우 storage package compile path만 연결하되 long-running loop는 만들지 않는다.

#### 테스트 작성

- 작성: `services/worker/internal/storage/postgres/mapping_test.go`
- 테스트명: `TestBacktestRunMappingRoundTrip`, `TestMarketBarMappingRejectsInvalidDecimal`
- 목표: domain type과 DB row helper 사이의 정상/경계 변환을 검증한다.

#### 중간 검증

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

예상 결과: storage ports와 Postgres adapter package가 fresh test로 통과한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/worker/go.mod` | STORAGE-1 |
| `services/worker/go.sum` | STORAGE-1 |
| `services/worker/tools.go` | STORAGE-1 |
| `services/worker/sqlc.yaml` | STORAGE-1 |
| `services/worker/internal/storage/postgres/migrations/*.sql` | STORAGE-1 |
| `services/worker/internal/storage/postgres/queries/*.sql` | STORAGE-1 |
| `services/worker/internal/storage/postgres/migrate.go` | STORAGE-1 |
| `services/worker/cmd/alt-worker-migrate/main.go` | STORAGE-1 |
| `bin/worker-storage-gen` | STORAGE-1 |
| `bin/worker-storage-check` | STORAGE-1 |
| `bin/test` | STORAGE-1 |
| `bin/build` | STORAGE-1 |
| `services/worker/internal/storage/ports.go` | STORAGE-2 |
| `services/worker/internal/storage/postgres/store.go` | STORAGE-2 |
| `services/worker/internal/storage/postgres/mapping.go` | STORAGE-2 |
| `services/worker/internal/storage/postgres/sqlc/*.go` | STORAGE-2 |
| `services/worker/internal/storage/postgres/*_test.go` | STORAGE-1, STORAGE-2 |
| `services/worker/cmd/alt-worker/main.go` | STORAGE-2 |

## 최종 검증

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

```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
bin/test
```

```bash
bin/lint
```

```bash
bin/build
```

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