<!-- task=m-runtime-scaffold-and-contract-baseline plan=0 tag=STORAGE -->

# Plan - STORAGE

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

`CODE_REVIEW-local-G05.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 필수다. 구현자는 아래 검증을 실행하고, 실제 stdout/stderr를 리뷰 스텁에 기록하고, active 파일은 그대로 둔 채 리뷰 준비를 보고한다. 최종 판정, 로그 rename, `complete.log`, archive 이동은 code-review 스킬 전용이다. 구현이 사용자만 결정할 수 있는 범위 변경, 사용자 소유 외부 환경/secret, 또는 scope conflict에 막히면 사용자에게 직접 묻지 말고 리뷰 스텁의 `사용자 리뷰 요청` 섹션에 결정 필요 사항과 근거를 채운 뒤 중단한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 닫을 수 있는 evidence gap은 사용자 리뷰 요청이 아니다.

## 배경

현재 `storage` 패키지는 `Ping`만 있는 placeholder라 migration의 repo/workspace/operation/event 원장과 코드 경계가 연결되어 있지 않다. 이 마일스톤의 남은 `storage-boundary` Task는 실제 DB 구현 전체가 아니라, 다음 repo registry/lease Milestone이 의존할 수 있는 store interface와 contract test 기준선을 닫는 작업이다. 외부 PostgreSQL 서비스나 driver 추가 없이 `database/sql` 이전의 경계와 migration 일치성부터 고정한다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 기록한다. 구현 중 직접 사용자에게 질문하거나 chat 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. code-review가 blocker 정당성을 검증하고 실제 `USER_REVIEW.md` 작성을 소유한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/control-plane-foundation/milestones/runtime-scaffold-and-contract-baseline.md`
- Task ids:
  - `storage-boundary`: Postgres migration과 storage boundary를 실제 store interface 기준으로 연결한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/control-plane-foundation/PHASE.md`
- `agent-roadmap/phase/control-plane-foundation/milestones/runtime-scaffold-and-contract-baseline.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/workspace-ops/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-test/local/workspace-ops-smoke.md`
- `.gitignore`
- `services/core/internal/storage/storage.go`
- `services/core/internal/core/model.go`
- `services/core/internal/events/events.go`
- `services/core/migrations/00001_initial.sql`

### 테스트 환경 규칙

- 선택한 `test_env`: `local`.
- `agent-test/local/rules.md`는 존재하고 읽었다. 적용 명령은 core baseline인 `cd services/core && go test ./...`, `cd services/core && go vet ./...`이다.
- `agent-test/local/core-smoke.md`는 읽었지만 `적용 범위`, `필수 검증`, `판정 기준`이 `<확인 필요>` skeleton이다. 따라서 구체 명령은 `agent-test/local/rules.md` baseline과 `services/core/go.mod` 기준 Go command로 fallback한다.
- `agent-test/local/workspace-ops-smoke.md`는 plan artifact 생성 때문에 읽었지만 skeleton이다. 별도 workspace-ops 검증 명령은 적용하지 않는다.
- Fresh storage contract 확인이 필요하므로 Go test cache는 핵심 명령에서 허용하지 않는다. `go test -count=1`을 사용한다.

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

- `storage` 패키지는 현재 test file이 없고 `Store` interface/contract를 검증하지 않는다.
- migration은 repo/workspace/operation/event 테이블을 만들지만, storage boundary가 해당 테이블을 반영하는지 확인하는 테스트가 없다.
- 이번 plan은 `storage_test.go`를 추가해 interface contract와 migration support를 동시에 고정한다.

### 심볼 참조

- 변경 후보: `storage.Store`, `storage.New`, `(*Store).Ping`.
- `rg --sort path "storage\\.New|storage\\.Store|\\bStore\\b|\\bPing\\(" services/core` 결과 외부 call site는 없고 `services/core/internal/storage/storage.go` 내부 정의만 있었다.
- 삭제/rename으로 인한 call-site update는 예상되지 않는다.

### 분할 판단

- split decision policy를 평가했다.
- 단일 plan으로 둔다. 변경 범위가 `core` 도메인의 `storage` 패키지와 같은 패키지 테스트에 머물고, API foundation과 call-site rollout이 분리되어 있지 않다.
- storage interface와 contract test는 서로를 검증하는 한 단위라 별도 subtask로 나누면 실제 완료 판정이 인위적으로 쪼개진다.
- DB driver, 실제 PostgreSQL CRUD, repo registry workflow는 제외하므로 storage/security/concurrency 위험은 bounded local review로 감당 가능하다.

### 범위 결정 근거

- 포함: `services/core/internal/storage/storage.go`, `services/core/internal/storage/storage_test.go`, 필요 시 `services/core/migrations/00001_initial.sql`의 최소 정합성 보정.
- 제외: 실제 PostgreSQL connection pool, driver dependency, SQL CRUD 구현, repo registry API, workspace lease allocation algorithm, operation worker execution, proto-socket handler wiring.
- 제외 이유: 이 마일스톤은 scaffold/contract baseline이고, 실제 repo/workspace 동작은 후속 `Repo Registry and Workspace Lease` Milestone 범위다.

### 빌드 등급

- `local-G05`: storage 경계와 테스트 추가는 public-ish internal API 설계지만 call site가 없고 검증이 Go unit/vet으로 deterministic하다.

## 구현 체크리스트

- [ ] `services/core/internal/storage/storage.go`를 repo/workspace/operation/event store interface와 boundary constructor 기준으로 교체한다.
- [ ] `services/core/internal/storage/storage_test.go`를 추가해 interface contract와 migration support를 검증한다. 검증: `cd services/core && go test -count=1 ./internal/storage` PASS.
- [ ] 전체 core baseline을 확인한다. 검증: `cd services/core && go test -count=1 ./...`와 `cd services/core && go vet ./...` PASS.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [STORAGE-1] Store boundary interface 작성

### 문제

`services/core/internal/storage/storage.go:5-14`는 `Ping`만 가진 placeholder라 migration과 core model이 요구하는 repo/workspace/operation/event store 경계를 표현하지 못한다.

```go
5 type Store struct{}
6
7 func New() *Store {
8 	return &Store{}
9 }
10
11 func (s *Store) Ping(ctx context.Context) error {
12 	_ = ctx
13 	return nil
14 }
```

`services/core/migrations/00001_initial.sql:3-56`에는 `repos`, `workspace_leases`, `operations`, `operation_events`, `revision_cursors`가 이미 있다.

### 해결 방법

`storage.go`를 실제 boundary contract로 바꾼다. 외부 DB driver는 추가하지 않는다.

```go
import (
	"context"

	"git.toki-labs.com/toki/gito/services/core/internal/core"
	"git.toki-labs.com/toki/gito/services/core/internal/events"
)

type Store interface {
	Ping(ctx context.Context) error
	Repos() RepoStore
	WorkspaceLeases() WorkspaceLeaseStore
	Operations() OperationStore
	OperationEvents() OperationEventStore
}

type RepoStore interface {
	SaveRepo(ctx context.Context, repo core.Repo) error
	GetRepo(ctx context.Context, id string) (core.Repo, error)
	ListRepos(ctx context.Context) ([]core.Repo, error)
}
```

`WorkspaceLeaseStore`, `OperationStore`, `OperationEventStore`도 같은 방식으로 `core.WorkspaceLease`, `core.Operation`, `events.Event`를 사용해 최소 CRUD 또는 append/list contract를 정의한다. `Boundary` concrete type과 `New(...) *Boundary` constructor는 주입받은 sub-store를 반환하며 `Ping`은 nil sub-store가 없는지 확인한다.

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

- [ ] `services/core/internal/storage/storage.go`: `core`와 `events` import 추가.
- [ ] `services/core/internal/storage/storage.go`: `Store`, `RepoStore`, `WorkspaceLeaseStore`, `OperationStore`, `OperationEventStore` interface 추가.
- [ ] `services/core/internal/storage/storage.go`: `OperationEvent` value type이 필요하면 `OperationID string`과 `Event events.Event`만 포함한다.
- [ ] `services/core/internal/storage/storage.go`: `Boundary`와 `New(...) *Boundary` constructor 추가.
- [ ] `services/core/internal/storage/storage.go`: `Ping`은 `context.Context` nil 여부와 필수 sub-store 주입 여부만 검증한다.

### 테스트 작성

`services/core/internal/storage/storage_test.go`에서 fake sub-store를 두고 `var _ Store = (*Boundary)(nil)`와 각 sub-store interface implementation을 compile-time로 검증한다. `New(...)`가 주입받은 store를 그대로 반환하는지도 확인한다.

### 중간 검증

```sh
cd services/core && go test -count=1 ./internal/storage
```

기대 결과: `ok git.toki-labs.com/toki/gito/services/core/internal/storage`.

## [STORAGE-2] Migration/interface contract test 추가

### 문제

`services/core/migrations/00001_initial.sql:3-56`의 테이블들은 storage interface의 backing schema지만 현재 테스트가 없다. 새 boundary가 migration에서 제공하지 않는 entity를 요구하거나, migration이 interface 핵심 테이블을 빠뜨려도 감지하지 못한다.

### 해결 방법

`storage_test.go`에 migration support test를 추가한다. SQL parser를 새로 들이지 말고, 현재 repo의 goose migration 파일을 읽어 interface가 요구하는 table/key column snippet이 있는지만 deterministic하게 확인한다.

```go
required := []string{
	"CREATE TABLE IF NOT EXISTS repos",
	"CREATE TABLE IF NOT EXISTS workspace_leases",
	"CREATE TABLE IF NOT EXISTS operations",
	"CREATE TABLE IF NOT EXISTS operation_events",
	"CREATE TABLE IF NOT EXISTS revision_cursors",
}
```

필요하면 `operation_events.operation_id`처럼 interface와 직접 맞물리는 column snippet도 추가한다.

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

- [ ] `services/core/internal/storage/storage_test.go`: migration 파일 `../../migrations/00001_initial.sql`을 읽는 test 추가.
- [ ] `services/core/internal/storage/storage_test.go`: repo/workspace/operation/event/revision cursor table snippet 확인.
- [ ] `services/core/internal/storage/storage_test.go`: interface contract test와 migration support test가 같은 package에서 통과하도록 정리.

### 테스트 작성

새 test 이름은 `TestBoundaryContract`와 `TestInitialMigrationSupportsStoreBoundary`로 둔다.

### 중간 검증

```sh
cd services/core && go test -count=1 ./internal/storage
```

기대 결과: `ok git.toki-labs.com/toki/gito/services/core/internal/storage`.

## 의존 관계 및 구현 순서

1. `STORAGE-1`에서 interface와 `Boundary`를 먼저 만든다.
2. `STORAGE-2`에서 같은 package test로 interface와 migration support를 검증한다.

## 수정 파일 요약

| 파일 | 항목 |
| --- | --- |
| `services/core/internal/storage/storage.go` | `STORAGE-1` |
| `services/core/internal/storage/storage_test.go` | `STORAGE-1`, `STORAGE-2` |
| `services/core/migrations/00001_initial.sql` | `STORAGE-2` only if a minimal schema mismatch is found |

## 최종 검증

```sh
cd services/core && go test -count=1 ./internal/storage
cd services/core && go test -count=1 ./...
cd services/core && go vet ./...
```

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