<!-- task=provider_neutral_plane_entrypoint/01_pipeline_mapper plan=0 tag=REFACTOR -->

# Provider-Neutral Work Item Task Mapper

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

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

## 배경

다음 로드맵 체크항목은 Plane 전용 task 생성 흐름을 provider-neutral 구조로 옮기는 것이다. 현재 mapper는 HTTP handler 안에서 `plane.WorkItem`을 바로 `workflow.CreateTaskInput`으로 바꾸므로, HTTP에서 Plane 타입을 끊기 전에 공통 work item mapper가 먼저 필요하다. 이 계획은 endpoint를 건드리지 않고 `internal/workitem` 안에 provider-neutral work item -> workflow task 변환 계약과 테스트를 먼저 만든다.

## 분석 결과

### 읽은 파일

- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/workspace-ops/rules.md`
- `agent-ops/roadmap/milestones/plane-task-pipeline-design.md`
- `services/core/go.mod`
- `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/router.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`

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

- `internal/workitem`은 ref normalization과 mapping lookup만 테스트한다. provider-neutral `workitem.WorkItem`을 `workflow.CreateTaskInput`으로 바꾸는 테스트가 없다.
- `internal/http/handlers_test.go`는 현재 `buildPlaneCreateTaskInput`이 Plane DTO를 직접 받는지 검증한다. 이 계획에서는 HTTP를 바꾸지 않으므로 해당 테스트는 유지하고, generic mapper 테스트를 `internal/workitem`에 추가한다.
- Plane adapter facet 테스트는 `FetchWorkItem` 변환까지만 검증한다. task 생성 mapper는 adapter가 아니라 core pipeline 책임으로 별도 테스트가 필요하다.

### 심볼 참조

- renamed/removed symbols: none.
- 이번 작업에서는 `buildPlaneCreateTaskInput`, `createPlaneTaskRequest.workItemRef`, `PlaneWorkItemClient`, `CreatePlaneTask`를 변경하지 않는다. 후속 `02+01_http_plane_entrypoint`에서 call-site 전환을 수행한다.

### 분할 판단

split decision policy를 먼저 평가했다. 이 체크항목은 provider-neutral foundation과 HTTP Plane call-site rollout이 함께 있으므로 split gate에 걸린다. 공유 task group은 `provider_neutral_plane_entrypoint`이고, `01_pipeline_mapper`는 의존성 없는 foundation 작업, `02+01_http_plane_entrypoint`는 `01_pipeline_mapper`의 `complete.log` 이후 실행되는 HTTP 전환 작업이다.

### 범위 결정 근거

- `services/core/internal/http/handlers.go`와 `handlers_test.go`는 이 계획에서 수정하지 않는다. HTTP import boundary 전환은 후속 dependent task가 담당한다.
- `services/core/internal/adapters/plane/client.go`는 수정하지 않는다. Plane adapter는 이미 `workitem.Reader`를 구현한다.
- `services/core/internal/workflow` 모델과 DB schema는 수정하지 않는다. mapper는 기존 `workflow.CreateTaskInput`을 생성하는 얇은 변환 계층으로 둔다.
- public HTTP route와 request/response shape는 변경하지 않는다.

### 빌드 등급

- build lane: `cloud-G06`
- review lane: `cloud-G06`
- 근거: 내부 provider-neutral pipeline 계약을 새로 만들고 후속 HTTP call-site refactor의 기반이 되므로 API/계약 판단 리스크가 있다.

## 구현 체크리스트

- [ ] `services/core/internal/workitem`에 provider-neutral work item task mapper 타입과 helper를 추가하고 단위 테스트를 작성한다.
- [ ] `gofmt`, 관련 Go 테스트, whitespace 검증을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 구현 항목

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

#### 문제

`services/core/internal/http/handlers.go:188-232`의 `buildPlaneCreateTaskInput`이 Plane request/ref/detail을 직접 받아 `workflow.CreateTaskInput`을 만든다. 이 로직이 HTTP package에 남아 있으면 후속 작업에서 `PlaneWorkItemClient`를 `workitem.Reader`로 바꿔도 core task 생성 payload와 external ref 생성 책임이 여전히 Plane endpoint에 묶인다.

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

```go
func buildPlaneCreateTaskInput(input createPlaneTaskRequest, workItem plane.WorkItem) (workflow.CreateTaskInput, error) {
```

After (`services/core/internal/workitem/provider.go`):

```go
import "github.com/nomadcode/nomadcode-core/internal/workflow"

type TaskCreateInput struct {
	Ref     Ref
	StateID string
	Comment string
}

func BuildCreateTaskInput(input TaskCreateInput, item WorkItem) (workflow.CreateTaskInput, error) {
	ref, err := NormalizeRef(input.Ref)
	if err != nil {
		return workflow.CreateTaskInput{}, err
	}
	// build title, message, provider-neutral metadata, payload, external ref
}
```

#### 해결 방법

- `services/core/internal/workitem/provider.go`에 `workflow` import를 추가한다.
- `TaskCreateInput`을 추가한다. 필드는 `Ref workitem.Ref`, `StateID`, `Comment`로 제한한다.
- `BuildCreateTaskInput(input TaskCreateInput, item WorkItem) (workflow.CreateTaskInput, error)`를 추가한다.
- `BuildCreateTaskInput`은 `NormalizeRef`를 호출해 provider/id와 JSON metadata를 검증한다.
- title은 `strings.TrimSpace(item.Title)` 우선, 비면 `ref.ID`를 사용한다.
- message는 `input.Comment`, `item.DescriptionText`, `item.DescriptionHTML`, title 순서로 `firstNonEmpty` 선택한다.
- payload는 provider-neutral key를 사용한다.

```go
metadata := map[string]string{
	"provider":     string(ref.Provider),
	"tenant":       ref.Tenant,
	"project":      ref.Project,
	"id":           ref.ID,
	"state_id":     strings.TrimSpace(input.StateID),
	"external_url": ref.URL,
}
payload := map[string]any{
	"message":   message,
	"work_item": metadata,
}
```

- `workflow.CreateTaskInput.Source`와 `External.Provider`는 `string(ref.Provider)`로 둔다.
- `External.ID`는 `ref.ID`, `External.URL`은 `ref.URL`, `External.Metadata`는 위 metadata JSON으로 둔다.
- 기존 `NormalizeRef`, `Mapping`, facet interface 동작은 변경하지 않는다.

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

- [ ] `services/core/internal/workitem/provider.go`에 `workflow` import를 추가한다.
- [ ] `TaskCreateInput`과 `BuildCreateTaskInput`을 추가한다.
- [ ] mapper 전용 `firstNonEmpty` helper가 필요하면 `workitem` package 안에 추가하되 HTTP helper와 이름 충돌이 없게 같은 package 안에서만 사용한다.
- [ ] `services/core/internal/workitem/provider_test.go`에 mapper 테스트를 추가한다.
- [ ] `services/core/go.mod`에 dependency 변화가 없음을 확인한다.

#### 테스트 작성

- 수정: `services/core/internal/workitem/provider_test.go`
- `TestBuildCreateTaskInputUsesCommentBeforeDescription`: comment가 description보다 우선하고 source/external provider가 `plane`으로 생성되는지 확인한다.
- `TestBuildCreateTaskInputStoresProviderNeutralMetadata`: `tenant`, `project`, `id`, `state_id`, `external_url` metadata와 `payload.work_item`이 provider-neutral key로 저장되는지 확인한다.
- `TestBuildCreateTaskInputFallsBackToRefIDAndRequiresValidRef`: title fallback과 provider/id 누락 error를 확인한다.

#### 중간 검증

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

기대 결과: workitem mapper 테스트가 fresh run으로 통과한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/workitem/provider.go` | REFACTOR-1 |
| `services/core/internal/workitem/provider_test.go` | REFACTOR-1 |
| `services/core/go.mod` | REFACTOR-1 dependency 변화 없음 확인 |

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

- `BuildCreateTaskInput`이 Plane package를 import하지 않는지 확인한다.
- mapper metadata와 payload가 `plane` 전용 key가 아니라 provider-neutral key를 쓰는지 확인한다.
- `NormalizeRef` 기존 동작과 기존 `workitem` tests가 깨지지 않았는지 확인한다.
- HTTP handler와 route는 이 task에서 바뀌지 않았는지 확인한다.

## 최종 검증

```bash
cd services/core && gofmt -w internal/workitem/*.go
```

기대 결과: 출력 없음. `internal/workitem` Go 파일이 gofmt 적용된다.

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

기대 결과: 관련 패키지 테스트가 fresh run으로 통과한다.

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

기대 결과: whitespace error가 없다.

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