<!-- task=m-milestone-work-item-creation-sync/05+03,04_idempotency_retry plan=1 tag=REVIEW_API -->

# Plan - Review API Idempotency Retry Fix

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

`CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 반드시 채운다. 사용자 전용 결정이나 외부 환경 차단은 review stub의 `사용자 리뷰 요청`에 근거를 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하지 않는다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
- Task ids:
  - `cycle-idempotency`: 같은 Plane 티켓 또는 같은 Milestone path 재처리 시 중복 생성 없이 남은 단계만 재시도한다.
- Completion mode: check-on-pass

## 배경

첫 리뷰(`code_review_cloud_G07_0.log`)는 identity map/retry slice를 FAIL로 판정했다. 선조회 후 upsert가 동시 재처리에서 같은 provider work item을 다른 Milestone path로 조용히 재바인딩할 수 있고, retry identity comparison이 `tenant`/`project`를 identity key로 비교하지 않아 project-scoped sync 계약과 어긋난다.

## 범위 결정 근거

- 이번 follow-up은 `code_review_cloud_G07_0.log`의 Required 이슈만 닫는다.
- Plane Todo projection, formatter, adapter body projection은 선행 subtask PASS 범위이므로 동작 변경하지 않는다.
- `./bin/sqlc`는 현재 `queries/workspace_slots.sql`의 기존 ambiguity로 실패할 수 있다. 재생성이 성공하면 산출물을 반영하고, 실패하면 정확한 출력과 함께 generated DB 코드가 수동 갱신되었음을 검증 결과에 남긴다.

## 구현 체크리스트

- [ ] `UpsertRoadmapSyncIdentityByWorkItem`가 기존 provider work item row의 Milestone path가 다르면 update하지 않고 typed conflict로 반환하게 SQL/query/store/tests를 보완한다.
- [ ] path unique race/raw unique violation을 `ErrRoadmapSyncIdentityConflict`로 변환하고 테스트를 추가한다.
- [ ] retry identity comparison이 provider, tenant, project, work item id, Milestone path를 같은 identity key로 비교하도록 보완하고 mismatch 테스트를 추가한다.
- [ ] `./bin/sqlc` 재생성 또는 차단 증거, `go test -count=1 ./internal/storage ./internal/roadmapsync ./internal/adapters/plane`, `go test ./...`, `git diff --check` 검증 결과를 남긴다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [REVIEW_API-1] Atomic Identity Conflict Handling

### 문제

`services/core/queries/roadmap_sync_identities.sql`의 upsert는 provider work item conflict 시 `roadmap_milestone_path`를 무조건 `EXCLUDED` 값으로 갱신한다. `services/core/internal/storage/roadmap_sync_identities.go`의 선조회는 원자적이지 않아, 같은 work item에 서로 다른 path가 동시에 들어오면 나중 upsert가 기존 identity를 재바인딩할 수 있다.

### 해결 방법

- upsert SQL의 `DO UPDATE`에 기존 `roadmap_milestone_path`가 incoming path와 같을 때만 update하는 조건을 둔다.
- 조건 불일치로 `RETURNING` row가 없을 때 `ErrRoadmapSyncIdentityConflict`로 변환한다.
- path unique constraint 위반도 raw pg error 대신 `ErrRoadmapSyncIdentityConflict`로 변환한다.
- mock 기반 단위 테스트로 query shape, no-row conflict mapping, unique violation mapping을 고정한다. 실제 DB race e2e가 없으면 생략 사유를 검증 결과에 남긴다.

### 수정 파일 후보

- `services/core/queries/roadmap_sync_identities.sql`
- `services/core/internal/db/roadmap_sync_identities.sql.go`
- `services/core/internal/storage/roadmap_sync_identities.go`
- `services/core/internal/storage/roadmap_sync_identities_test.go`

### 중간 검증

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

## [REVIEW_API-2] Strict Retry Identity Key

### 문제

`roadmapsync.Identity.Matches`는 provider, work item id, Milestone path만 비교한다. 이번 plan과 DB unique key는 provider, tenant, project, work item id를 provider-side identity key로 저장하므로 retry decision도 다른 Plane workspace/project를 같은 identity로 재사용하면 안 된다.

### 해결 방법

- retry decision에서 쓰는 identity comparison이 `provider`, `tenant`, `project`, `work_item_id`, `roadmap_milestone_path`를 모두 비교하도록 보완한다.
- 기존 broader matching이 다른 call site에 필요하면 retry 전용 strict matcher를 분리하고 `ReconcileCreationCycle`에서만 strict matcher를 사용한다.
- tenant/project mismatch가 `ActionConflict`가 되는 테스트를 추가한다. develop match 동작을 함께 바꾸는 경우 기존 parser/match 테스트도 업데이트한다.

### 수정 파일 후보

- `services/core/internal/roadmapsync/identity.go`
- `services/core/internal/roadmapsync/retry.go`
- `services/core/internal/roadmapsync/identity_test.go`
- `services/core/internal/roadmapsync/retry_test.go`

### 중간 검증

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

## 최종 검증

```bash
cd services/core && ./bin/sqlc
cd services/core && go test -count=1 ./internal/storage ./internal/roadmapsync ./internal/adapters/plane
cd services/core && go test ./...
git diff --check
```

`./bin/sqlc`가 기존 `workspace_slots` query ambiguity로 실패하면, exact stdout/stderr를 `CODE_REVIEW-cloud-G07.md`에 남기고 신규 generated 파일의 수동 변경 근거를 함께 기록한다.
