<!-- task=m-operation-event-outbox/01_operation_store plan=0 tag=API -->

# Operation Store Boundary

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채워야 구현이 끝난다. 검증 명령을 실행하고 출력 원문을 붙인 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 종결, log rename, `complete.log`, archive 이동은 code-review-skill 전용이다. 사용자 결정, 사용자 소유 외부 환경, scope 충돌 없이는 진행할 수 없으면 review stub의 `사용자 리뷰 요청`에 근거를 기록하고 중단한다. 구현 중 사용자에게 직접 묻거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 재실행으로 채울 수 있는 검증 증거 공백은 사용자 리뷰 요청이 아니다.

## 배경

현재 operation type/state/idempotency 모델과 SQL 제약은 정리됐지만, `storage.OperationStore`는 `SaveOperation` 수준이고 `PgStore.Operations()`는 `nil`을 반환한다. `op-store` 마일스톤 task는 create/start/succeed/fail/cancel 전환과 idempotency duplicate 처리를 Postgres 원장 경계로 고정해야 한다.

## 사용자 리뷰 요청 흐름

구현 중 차단은 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/control-plane-foundation/milestones/operation-event-outbox.md`
- Task ids:
  - `op-store`: operation create/start/succeed/fail/cancel store boundary를 만든다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/control-plane-foundation/PHASE.md`
- `agent-roadmap/phase/control-plane-foundation/milestones/operation-event-outbox.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `services/core/go.mod`
- `services/core/internal/core/model.go`
- `services/core/internal/core/model_test.go`
- `services/core/internal/storage/storage.go`
- `services/core/internal/storage/postgres.go`
- `services/core/internal/storage/storage_test.go`
- `services/core/internal/storage/postgres_test.go`
- `services/core/internal/storage/postgres_repo_test.go`
- `services/core/migrations/00001_initial.sql`

### 테스트 환경 규칙

- test_env: `local`.
- `agent-test/local/rules.md` present/read. Matching profile: `agent-test/local/core-smoke.md`.
- Applied command contract: core changes require `cd services/core && go test ./...`; optional `go vet ./...`/`go build ./...` may be added, but final verification here uses fresh `go test -count=1 ./...`.
- No `<확인 필요>` command values were found. Postgres integration tests depend on `GITO_TEST_DATABASE_URL`; when unset they skip by existing test contract.

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

- Operation create/idempotency duplicate: missing today. Add storage unit validation and Postgres integration coverage.
- Operation invalid transitions: core state helpers now exist, but store transition methods have no tests. Add focused transition tests.
- Operation list/get scan ordering: missing today. Add integration assertions.

### 심볼 참조

- Planned interface change removes/replaces `SaveOperation`; call sites found only in `services/core/internal/storage/storage_test.go` fake implementation. No production call sites exist.

### 분할 판단

- Split policy evaluated before file selection.
- Shared task group: `agent-task/m-operation-event-outbox/`.
- `01_operation_store`: independent foundation. Implements operation store boundary.
- `02+01_operation_pick`: depends on `01_operation_store` because picking needs durable operation state transitions.
- This file has no predecessor.

### 범위 결정 근거

- Exclude worker pick and runner behavior; handled by `02+01_operation_pick`.
- Exclude durable outbox event append/list/publish; belongs to outbox epic.
- Exclude proto-socket contracts/UI/provider projection; out of this milestone task.

### 빌드 등급

- Build: `local-G06`. Review: `cloud-G05`.
- Rationale: bounded Go storage implementation, but storage/migration/idempotency state transitions need careful review and deterministic tests.

## 구현 체크리스트

- [ ] `OperationStore` 인터페이스를 create/get/list/start/succeed/fail/cancel 경계로 바꾸고 기존 fake/test compile break를 정리한다.
- [ ] `PgStore.Operations()`가 실제 Postgres operation store를 반환하게 구현한다.
- [ ] operation normalization, idempotency duplicate, invalid transition 테스트를 작성한다. 검증: invalid transition과 idempotency duplicate를 테스트한다.
- [ ] `go test -count=1 ./internal/core ./internal/storage`와 `go test -count=1 ./...`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] OperationStore lifecycle API

#### 문제

- [services/core/internal/storage/storage.go](/config/workspace/gito/services/core/internal/storage/storage.go:53)의 `OperationStore`는 `SaveOperation`만 있어 create/start/succeed/fail/cancel 상태 전환과 idempotency duplicate 결과를 표현하지 못한다.

Before:

```go
53 type OperationStore interface {
54     SaveOperation(ctx context.Context, op core.Operation) error
55     GetOperation(ctx context.Context, id string) (core.Operation, error)
56     ListOperations(ctx context.Context, repoID string) ([]core.Operation, error)
57 }
```

#### 해결 방법

- `ErrInvalidOperation`, `ErrOperationAlreadyExists`, `ErrOperationNotFound`를 추가한다.
- `OperationCreateResult{Operation core.Operation, Created bool}`를 추가한다.
- `CreateOperation`, `GetOperation`, `ListOperations`, `StartOperation`, `SucceedOperation`, `FailOperation`, `CancelOperation`으로 interface를 확정한다.
- `Start/Succeed/Fail/Cancel`은 `core.OperationState.CanTransitionTo`를 기준으로 invalid transition을 거부한다.

After:

```go
type OperationCreateResult struct {
    Operation core.Operation
    Created   bool
}

type OperationStore interface {
    CreateOperation(ctx context.Context, op core.Operation) (OperationCreateResult, error)
    GetOperation(ctx context.Context, id string) (core.Operation, error)
    ListOperations(ctx context.Context, repoID string) ([]core.Operation, error)
    StartOperation(ctx context.Context, id string, now time.Time) (core.Operation, error)
    SucceedOperation(ctx context.Context, id string, now time.Time) (core.Operation, error)
    FailOperation(ctx context.Context, id string, now time.Time) (core.Operation, error)
    CancelOperation(ctx context.Context, id string, now time.Time) (core.Operation, error)
}
```

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

- [ ] `services/core/internal/storage/storage.go`: errors/result/interface 갱신.
- [ ] `services/core/internal/storage/storage_test.go`: fakeOps와 boundary contract compile check 갱신.

#### 테스트 작성

- `services/core/internal/storage/storage_test.go`에서 interface compile check를 유지한다.

#### 중간 검증

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

Expected: storage package passes or Postgres integration cases skip only when `GITO_TEST_DATABASE_URL` is unset.

### [API-2] Postgres operation store

#### 문제

- [services/core/internal/storage/postgres.go](/config/workspace/gito/services/core/internal/storage/postgres.go:19)의 `PgStore`에는 operation sub-store field가 없고, [services/core/internal/storage/postgres.go](/config/workspace/gito/services/core/internal/storage/postgres.go:61)는 `nil`을 반환한다.

Before:

```go
19 type PgStore struct {
20     pool       *pgxpool.Pool
21     repos      *pgRepoStore
22     leases     *pgWorkspaceLeaseStore
23     watches    *pgBranchWatchStore
24     cursors    *pgRevisionCursorStore
25     deliveries *pgProviderDeliveryStore
}
...
61 func (s *PgStore) Operations() OperationStore { return nil }
```

#### 해결 방법

- `ops *pgOperationStore` field를 추가하고 `NewPgStore`에서 초기화한다.
- `pgOperationStore`를 `pgWorkspaceLeaseStore`와 같은 파일에 추가한다.
- `CreateOperation`은 normalize 후 insert한다. `idempotency_key`가 있으면 unique conflict에서 기존 row를 select하고 `Created=false`를 반환한다. id collision은 `ErrOperationAlreadyExists`로 반환한다.
- `Start/Succeed/Fail/Cancel`은 current row를 읽고 `CanTransitionTo` 검증 후 `UPDATE ... WHERE id=$1 AND state=$old`로 저장한다.
- `ListOperations`는 `repo_id` 필터와 `created_at, id` ordering을 사용한다.

After:

```go
type PgStore struct {
    pool       *pgxpool.Pool
    repos      *pgRepoStore
    leases     *pgWorkspaceLeaseStore
    ops        *pgOperationStore
    watches    *pgBranchWatchStore
    cursors    *pgRevisionCursorStore
    deliveries *pgProviderDeliveryStore
}

func (s *PgStore) Operations() OperationStore { return s.ops }
```

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

- [ ] `services/core/internal/storage/postgres.go`: `pgOperationStore`, normalization, scanner, lifecycle methods 구현.
- [ ] `services/core/internal/storage/postgres_repo_test.go`: DB 없이 가능한 invalid operation normalization 테스트 추가.
- [ ] `services/core/internal/storage/postgres_test.go`: DB integration으로 create/list/get/idempotency duplicate/transition 테스트 추가.
- [ ] `services/core/migrations/00001_initial.sql`: 이미 정리된 operation constraints와 구현 쿼리가 맞는지 확인만 한다.

#### 테스트 작성

- `TestPgOperationStoreValidatesBeforeDatabase`: required fields, invalid type/state, non-queued create state를 검증한다.
- `TestPostgresOperationStoreLifecycleAndIdempotency`: create duplicate idempotency returns existing with `Created=false`; queued->running->succeeded succeeds; succeeded->failed fails with `ErrInvalidOperation`; list ordering is stable.

#### 중간 검증

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

Expected: storage package passes; DB-specific tests may skip only when `GITO_TEST_DATABASE_URL` is unset.

## 수정 파일 요약

| 파일 | 항목 |
| --- | --- |
| `services/core/internal/storage/storage.go` | API-1 |
| `services/core/internal/storage/storage_test.go` | API-1 |
| `services/core/internal/storage/postgres.go` | API-2 |
| `services/core/internal/storage/postgres_repo_test.go` | API-2 |
| `services/core/internal/storage/postgres_test.go` | API-2 |
| `services/core/migrations/00001_initial.sql` | API-2 |

## 최종 검증

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

Expected: all packages pass; Postgres integration tests skip only if `GITO_TEST_DATABASE_URL` is unset.

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