<!-- task=provider_neutral_plane_entrypoint/03_generic_pipeline_service plan=0 tag=REFACTOR -->

# Work Item Generic Pipeline Service Refactor

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

`CODE_REVIEW-cloud-G06.md`의 구현 에이전트 소유 섹션을 채우는 것은 필수다. 구현 후 검증 명령을 실제로 실행하고, 실제 출력과 주요 설계 결정, 계획 대비 변경 사항을 review stub에 기록한 뒤 active 파일을 그대로 둔 채 리뷰 준비 상태로 보고한다. 최종 판정, `.log` 아카이브, `complete.log` 작성, archive 이동은 code-review-skill 전용이다.

## 배경

`POST /api/integrations/plane/tasks`는 compatibility endpoint로 유지됐지만, handler가 아직 work item 조회, mapper 호출, workflow task 생성 순서를 직접 조립한다. Milestone의 남은 하위 항목은 이 Plane entrypoint 흐름을 provider-neutral generic pipeline service 아래로 옮기는 것이다. 이번 작업은 새 generic endpoint나 enqueue 정책을 만들지 않고, 기존 Plane endpoint 내부 orchestration 위치만 HTTP 밖으로 뺀다.

## 분석 결과

### 읽은 파일

- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/roadmap/milestones/plane-task-pipeline-design.md`
- `agent-task/archive/2026/05/provider_neutral_plane_entrypoint/01_pipeline_mapper/complete.log`
- `agent-task/archive/2026/05/provider_neutral_plane_entrypoint/02+01_http_plane_entrypoint/complete.log`
- `services/core/cmd/server/main.go`
- `services/core/internal/adapters/plane/client.go`
- `services/core/internal/adapters/plane/client_test.go`
- `services/core/internal/http/handlers.go`
- `services/core/internal/http/handlers_test.go`
- `services/core/internal/http/middleware_test.go`
- `services/core/internal/http/router.go`
- `services/core/internal/storage/store.go`
- `services/core/internal/workflow/model.go`
- `services/core/internal/workflow/service.go`
- `services/core/internal/workflow/service_test.go`
- `services/core/internal/workitem/provider.go`
- `services/core/internal/workitem/provider_test.go`

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

- 새 generic pipeline service는 아직 없다. 기존 tests는 `workitem.BuildCreateTaskInput` mapper와 HTTP helper만 검증한다.
- `CreatePlaneTask`가 `FetchWorkItem` -> mapper -> `workflow.CreateTask` 순서를 직접 수행하지 않게 되는지 검증하는 테스트가 없다.
- `cmd/server`는 `*plane.Client`를 `NewHandler`에 넘기는 compile coverage가 있으나, 새 pipeline service 생성 경로는 아직 없다.

### 심볼 참조

- `WorkItemReader`: `services/core/internal/http/handlers.go:21-23`, `handlers.go:28`, `handlers.go:32`. 제거하고 `NewHandler` parameter는 `workitem.Reader`를 받되 handler field는 pipeline service interface로 바꾼다.
- `Handler.workItems`: `services/core/internal/http/handlers.go:28`, `handlers.go:36`, `handlers.go:88`, `handlers.go:105`. `workItemTasks` 또는 동등한 provider-neutral pipeline field로 교체한다.
- `buildPlaneCreateTaskInput`: `services/core/internal/http/handlers.go:192-202`, `services/core/internal/http/handlers_test.go:38-101`. HTTP package에서 제거하고 mapper 검증은 `workitem`/pipeline service tests로 유지한다.
- `workitem.BuildCreateTaskInput`: 현재 `services/core/internal/http/handlers.go:197`, `services/core/internal/workitem/provider_test.go` 여러 테스트에서 참조한다. 구현 후 HTTP 직접 참조는 없어야 하고, 새 pipeline service가 참조한다.
- `FetchWorkItem`: 현재 HTTP handler와 Plane adapter/tests에서 참조한다. 구현 후 HTTP 직접 호출은 없어야 하고, 새 pipeline service와 adapter/tests만 참조한다.
- `NewHandler`: `services/core/cmd/server/main.go:87`, `services/core/internal/http/middleware_test.go:47`. Signature 호환 또는 최소 call-site 수정이 필요하다.

### 분할 판단

split decision policy를 먼저 평가했다. 이 작업은 이전 split group `provider_neutral_plane_entrypoint`의 남은 세 번째 subtask이며, 선행 `01_pipeline_mapper`와 `02+01_http_plane_entrypoint`는 이미 archive `complete.log`가 있다. 이번 subtask 내부에서는 새 service 추가와 HTTP call-site 전환이 한 route에만 묶여 있고, service가 unused 상태로 먼저 완료되는 것보다 한 리뷰에서 compile/test로 닫는 편이 더 작다. 따라서 추가 sibling으로 쪼개지 않고 `agent-task/provider_neutral_plane_entrypoint/03_generic_pipeline_service/` 단일 active plan을 사용한다.

### 범위 결정 근거

- 새 generic HTTP endpoint는 만들지 않는다. 기존 `/api/integrations/plane/tasks` route와 request/response shape를 유지한다.
- enqueue 조건, webhook/polling, idempotency, status/comment projection 구현은 건드리지 않는다.
- Plane adapter raw methods와 smoke CLI는 provider direct boundary이므로 변경하지 않는다.
- `workitem.BuildCreateTaskInput` mapper semantics와 metadata key는 이전 완료 작업의 산출물이므로 바꾸지 않는다.
- DB schema, SQLC, scheduler, notification은 이번 orchestration 이동에 필요하지 않아 제외한다.

### 빌드 등급

- build lane: `cloud-G06`
- review lane: `cloud-G06`
- 근거: 내부 service 경계 신설과 HTTP call-site 전환이 함께 있고, provider-neutral package boundary와 compile/test 검증이 필요하다.

## 구현 체크리스트

- [ ] `services/core/internal/workitempipeline`에 provider-neutral task creation service를 추가한다.
- [ ] `services/core/internal/http`의 Plane compatibility handler가 work item 조회/mapper/workflow 생성 순서를 직접 조립하지 않고 pipeline service를 호출하게 한다.
- [ ] pipeline service와 HTTP boundary 테스트를 갱신하고 기존 route/request/response compatibility를 유지한다.
- [ ] `gofmt`, provider boundary 검색, 관련 Go 테스트, whitespace 검증을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 구현 항목

### [REFACTOR-1] Provider-neutral work item task pipeline service를 추가한다

#### 문제

`services/core/internal/http/handlers.go:105-117`에서 compatibility HTTP handler가 provider work item 조회, `workitem.BuildCreateTaskInput`, `workflow.CreateTask` 호출을 직접 순서화한다. 이 때문에 Plane compatibility endpoint는 request DTO만 Plane-specific일 뿐, core task pipeline orchestration이 HTTP package 안에 남아 있다.

Before (`services/core/internal/http/handlers.go:105-117`):

```go
item, err := h.workItems.FetchWorkItem(r.Context(), ref)
if err != nil {
	h.writeServiceError(w, err)
	return
}

createInput, err := buildPlaneCreateTaskInput(input, item)
if err != nil {
	writeError(w, stdhttp.StatusBadRequest, err.Error())
	return
}

task, err := h.workflow.CreateTask(r.Context(), createInput)
```

After (`services/core/internal/workitempipeline/service.go`):

```go
func (s *Service) CreateTaskFromWorkItem(ctx context.Context, input CreateTaskInput) (storage.Task, error) {
	item, err := s.reader.FetchWorkItem(ctx, input.Ref)
	if err != nil {
		return storage.Task{}, err
	}
	createInput, err := workitem.BuildCreateTaskInput(workitem.TaskCreateInput{
		Ref:     input.Ref,
		StateID: input.StateID,
		Comment: input.Comment,
	}, item)
	if err != nil {
		return storage.Task{}, err
	}
	return s.tasks.CreateTask(ctx, createInput)
}
```

#### 해결 방법

- 새 package `services/core/internal/workitempipeline`을 만든다.
- `service.go`에 `CreateTaskInput`, `TaskCreator`, `Service`, `New`, `CreateTaskFromWorkItem`을 정의한다.
- `TaskCreator`는 `workflow.Service.CreateTask`와 정확히 맞게 `CreateTask(context.Context, workflow.CreateTaskInput) (storage.Task, error)`를 요구한다.
- `Service`는 `workitem.Reader`로 work item을 읽고, `workitem.BuildCreateTaskInput`으로 `workflow.CreateTaskInput`을 만든 뒤, `TaskCreator.CreateTask`를 호출한다.
- nil dependency 처리는 HTTP가 기존처럼 service 미구성 상태를 503으로 막는 것을 우선한다. Service 단위 테스트에서는 reader/task creator error propagation을 검증한다.

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

- [ ] `services/core/internal/workitempipeline/service.go`를 추가한다.
- [ ] `CreateTaskInput{Ref, StateID, Comment}`를 정의한다.
- [ ] `TaskCreator` interface를 `workflow.Service.CreateTask` signature와 일치시킨다.
- [ ] `CreateTaskFromWorkItem`에서 reader fetch error와 task create error를 그대로 반환한다.
- [ ] mapper semantics는 `workitem.BuildCreateTaskInput`에 위임하고 복제하지 않는다.

#### 테스트 작성

- 추가: `services/core/internal/workitempipeline/service_test.go`
- `TestCreateTaskFromWorkItemFetchesMapsAndCreatesTask`: fake reader와 fake task creator를 사용해 ref 전달, payload message, provider-neutral metadata, returned task id/status를 검증한다.
- `TestCreateTaskFromWorkItemPropagatesReaderError`: reader error가 task creator 호출 없이 반환되는지 검증한다.
- `TestCreateTaskFromWorkItemPropagatesCreateTaskError`: task creator error가 반환되는지 검증한다.

#### 중간 검증

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

기대 결과: 새 pipeline service 단위 테스트가 fresh run으로 통과한다.

### [REFACTOR-2] Plane compatibility handler를 pipeline service 호출로 전환한다

#### 문제

`services/core/internal/http/handlers.go:21-32`의 HTTP package는 `WorkItemReader` field를 직접 보관하고, `CreatePlaneTask`가 provider work item fetch와 mapper 호출을 직접 수행한다. Milestone의 남은 하위 항목은 이 흐름을 generic pipeline service 아래로 옮기는 것이므로 HTTP는 Plane request body를 provider-neutral input으로 변환하고 service 호출만 해야 한다.

Before (`services/core/internal/http/handlers.go:21-32`):

```go
type WorkItemReader interface {
	FetchWorkItem(ctx context.Context, ref workitem.Ref) (workitem.WorkItem, error)
}

type Handler struct {
	db        *pgxpool.Pool
	workflow  *workflow.Service
	workItems WorkItemReader
	logger    *slog.Logger
}
```

After (`services/core/internal/http/handlers.go`):

```go
type WorkItemTaskCreator interface {
	CreateTaskFromWorkItem(ctx context.Context, input workitempipeline.CreateTaskInput) (storage.Task, error)
}

type Handler struct {
	db            *pgxpool.Pool
	workflow      *workflow.Service
	workItemTasks WorkItemTaskCreator
	logger        *slog.Logger
}
```

#### 해결 방법

- `handlers.go`에서 local `WorkItemReader` interface와 `buildPlaneCreateTaskInput` helper를 제거한다.
- `NewHandler`는 기존 call-site compatibility를 유지하기 위해 `workItemReader workitem.Reader` parameter를 계속 받고, `workflowService`와 reader가 둘 다 있을 때 `workitempipeline.New(workItemReader, workflowService)`를 생성해 handler field에 넣는다.
- `CreatePlaneTask`는 기존 nil check와 error message `plane client is not configured`를 유지한다.
- `CreatePlaneTask`는 request body를 `workitem.Ref`로 변환한 뒤 `workitempipeline.CreateTaskInput{Ref, StateID, Comment}`만 service에 전달한다.
- `handlers_test.go`의 `buildPlaneCreateTaskInput` 테스트는 제거하거나 pipeline service 테스트로 대체한다. HTTP package에는 `workItemRef` mapping 테스트와 handler가 service에 input을 넘기는 focused test를 둔다.
- `middleware_test.go:47`과 `cmd/server/main.go:87`의 `NewHandler` call-site compile이 깨지지 않게 한다.

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

- [ ] `services/core/internal/http/handlers.go`에서 `WorkItemReader`, `workItems`, `buildPlaneCreateTaskInput`을 제거한다.
- [ ] `NewHandler`가 `workitem.Reader`와 `workflow.Service`로 pipeline service를 구성하게 한다.
- [ ] `CreatePlaneTask`가 `workItemTasks.CreateTaskFromWorkItem`만 호출하게 한다.
- [ ] 기존 route `/api/integrations/plane/tasks`와 response keys `id`, `status`, `external_provider`, `external_id`를 유지한다.
- [ ] `services/core/cmd/server/main.go`와 `services/core/internal/http/middleware_test.go` call-site compile을 유지한다.

#### 테스트 작성

- 수정: `services/core/internal/http/handlers_test.go`
- 유지: `TestPlaneTaskRequestBuildsProviderNeutralRef`.
- 추가: `TestCreatePlaneTaskDelegatesToWorkItemPipeline` 또는 동등한 이름. `Handler{workflow: fakeOrNil, workItemTasks: fakeWorkItemTaskCreator}`로 JSON request를 보내고, fake가 받은 `workitempipeline.CreateTaskInput`의 ref/state/comment와 HTTP response shape를 검증한다.
- 추가 또는 유지: nil pipeline일 때 기존 `503`과 `plane client is not configured` response가 유지되는지 검증한다.
- 삭제: `TestBuildPlaneCreateTaskInputUsesCommentBeforeDescription`, `TestBuildPlaneCreateTaskInputStoresExternalMetadata`는 HTTP 직접 helper가 사라지므로 pipeline/workitem tests로 대체한다.

#### 중간 검증

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

기대 결과: HTTP handler tests가 fresh run으로 통과하고 route compatibility가 유지된다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/workitempipeline/service.go` | REFACTOR-1 |
| `services/core/internal/workitempipeline/service_test.go` | REFACTOR-1 |
| `services/core/internal/http/handlers.go` | REFACTOR-2 |
| `services/core/internal/http/handlers_test.go` | REFACTOR-2 |
| `services/core/internal/http/middleware_test.go` | REFACTOR-2 compile 확인 only |
| `services/core/cmd/server/main.go` | REFACTOR-2 compile 확인 only |

## 리뷰어를 위한 체크포인트

- `services/core/internal/http`에서 `FetchWorkItem`, `workitem.BuildCreateTaskInput`, `workitem.TaskCreateInput`, `workitem.WorkItem` 직접 참조가 사라졌는지 확인한다.
- 새 `workitempipeline` package가 provider-neutral `workitem.Reader`와 `workflow.CreateTaskInput` mapper만 연결하고 Plane adapter 타입을 import하지 않는지 확인한다.
- 기존 `/api/integrations/plane/tasks` route, request body, response shape가 유지되는지 확인한다.
- `cmd/server`가 기존처럼 `*plane.Client`를 `NewHandler`에 넘겨도 compile되는지 확인한다.
- mapper semantics를 HTTP나 pipeline에 복제하지 않고 `workitem.BuildCreateTaskInput`에 위임했는지 확인한다.

## 최종 검증

```bash
cd services/core && gofmt -w internal/workitempipeline/service.go internal/workitempipeline/service_test.go internal/http/handlers.go internal/http/handlers_test.go internal/http/middleware_test.go
```

기대 결과: 출력 없음. 새 package와 HTTP Go 파일이 gofmt 적용된다.

```bash
cd services/core && ! rg --sort path -n 'FetchWorkItem\\(|BuildCreateTaskInput\\(|workitem\\.TaskCreateInput|workitem\\.WorkItem' internal/http
```

기대 결과: 출력 없음. HTTP package가 provider fetch/mapper/detail DTO를 직접 조립하지 않는다.

```bash
cd services/core && go test -count=1 ./internal/workitempipeline ./internal/http ./internal/workitem ./cmd/server
```

기대 결과: pipeline, HTTP, workitem tests가 fresh run으로 통과하고 `cmd/server`가 compile된다.

```bash
cd services/core && go test -count=1 ./internal/adapters/plane ./internal/workflow
```

기대 결과: Plane adapter reader boundary와 workflow task creator boundary가 fresh run으로 통과한다.

```bash
git diff --check
```

기대 결과: whitespace error가 없다.

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