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

# Plan - API Idempotency Retry

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

`CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 반드시 채운다. 검증 명령을 실행하고 실제 출력과 구현 메모를 남긴 뒤 active 파일을 유지한 채 리뷰 준비를 보고한다. 사용자 전용 결정이나 외부 환경 차단은 review stub의 `사용자 리뷰 요청`에 근거를 기록하고 멈춘다.

## 배경

Plane-origin 생성 sync는 중간 단계가 일부만 성공해도 같은 Plane 티켓 또는 Milestone path 재처리 시 중복 생성 없이 남은 단계만 재시도해야 한다. 03은 develop match, 04는 Plane projection을 만들며, 이 plan은 그 둘을 identity map과 step ledger로 묶어 idempotent cycle로 만든다.

## 사용자 리뷰 요청 흐름

구현 중 차단은 active 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

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/domain/core/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
- `services/core/internal/workitem/provider.go`
- `services/core/internal/workitempipeline/service.go`
- `services/core/internal/storage/project_sync_settings.go`
- `services/core/internal/storage/workspace_slots.go`
- `services/core/migrations/00004_create_project_sync_settings.sql`
- `services/core/migrations/00005_create_workspace_slots.sql`
- `packages/contracts/notes/flutter-core-api-candidates.md`

### 테스트 환경 규칙

- test_env: `local`.
- `agent-test/local/core-smoke.md` 기준으로 `cd services/core && go test ./...` 실행.
- DB migration/query 변경은 generated db code와 storage tests를 함께 갱신한다.

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

- 실제 Plane/network retry는 fake projector와 fake store로 검증한다.
- DB race는 unique index/upsert query shape와 unit test로 검증하고 multi-process DB e2e는 제외한다.

### 심볼 참조

- renamed/removed symbols: none.

### 분할 판단

- 이 plan은 `05+03,04_idempotency_retry`이며 predecessors `03_develop_match`, `04_plane_todo_projection` complete.log가 필요하다. 현재 both missing.

### 범위 결정 근거

- Plane child work item, `Todo -> In Progress`, `Done/User Review/Cancelled` lifecycle은 Milestone 범위 제외이므로 구현하지 않는다.
- full saga/orchestrator framework는 과하고, first slice는 identity map + step ledger + retry decision으로 제한한다.

### 빌드 등급

- build: `cloud-G07`. persistence, uniqueness, retry ordering의 회귀 위험이 높다.

## 구현 체크리스트

- [ ] Milestone path, provider, tenant, project, work item id, provider revision, roadmap revision을 저장하는 identity map migration/query/store를 추가한다.
- [ ] provider work item 또는 Milestone path 재처리 시 existing identity를 찾고 중복 Milestone 생성/authoring을 막는다.
- [ ] develop match, comment preserve, body/title update, Todo status move의 step ledger를 저장하고 성공한 step은 재시도하지 않는다.
- [ ] actor guard와 revision mismatch가 silent overwrite 대신 conflict/retryable result를 반환하게 한다.
- [ ] `cd services/core && go test ./...`와 `git diff --check`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- `03_develop_match`의 `complete.log`가 필요하다. 현재 missing.
- `04_plane_todo_projection`의 `complete.log`가 필요하다. 현재 missing.

### [API-1] Identity Map Persistence

#### 문제

contract note는 identity shape와 idempotency key를 요구하지만([flutter-core-api-candidates.md](/config/workspace/nomadcode/packages/contracts/notes/flutter-core-api-candidates.md:421)), core DB에는 project sync setting과 workspace slot만 있고 Milestone/work item identity map이 없다.

#### 해결 방법

`roadmap_sync_identities` migration과 SQLC queries를 추가한다. unique constraints는 `(provider, tenant, project, work_item_id)`와 `(roadmap_milestone_path)`를 둔다. revision fields는 nullable text로 둔다.

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

- [ ] `services/core/migrations/00006_create_roadmap_sync_identities.sql`
- [ ] `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`

#### 테스트 작성

- upsert by provider work item returns same row.
- upsert by milestone path returns same row.
- mismatched provider work item/path pair returns conflict sentinel.

#### 중간 검증

```bash
cd services/core && go test ./internal/storage ./internal/db
```

### [API-2] Step Ledger

#### 문제

Milestone은 부분 실패 후 남은 단계만 재시도해야 한다([milestone-work-item-creation-sync.md](/config/workspace/nomadcode/agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md:105)). 현재 어느 projection step이 성공했는지 저장하지 않는다.

#### 해결 방법

`roadmap_sync_steps` migration/query/store를 추가한다. steps는 `develop_matched`, `original_comment_preserved`, `plane_body_updated`, `plane_todo_moved`로 제한한다. completed step은 idempotently skip한다.

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

- [ ] `services/core/migrations/00007_create_roadmap_sync_steps.sql`
- [ ] `services/core/queries/roadmap_sync_steps.sql`
- [ ] `services/core/internal/db/roadmap_sync_steps.sql.go`
- [ ] `services/core/internal/storage/roadmap_sync_steps.go`
- [ ] `services/core/internal/storage/roadmap_sync_steps_test.go`

#### 테스트 작성

- completed step 재적용은 no-op.
- failed step 이후 재시도는 해당 step부터 재개.
- unknown step은 reject.

#### 중간 검증

```bash
cd services/core && go test ./internal/storage
```

### [API-3] Retry Decision Service

#### 문제

03/04가 각각 match와 projection을 수행해도 재처리 시 어떤 단계를 건너뛰고 어디서 재개할지 결정하는 service가 없다.

#### 해결 방법

`roadmapsync.ReconcileCreationCycle`을 추가해 identity map과 step ledger를 읽고 next action을 반환한다. actor가 NomadCode self mutation이면 trigger로 처리하지 않는 actor guard를 둔다.

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

- [ ] `services/core/internal/roadmapsync/retry.go`
- [ ] `services/core/internal/roadmapsync/retry_test.go`

#### 테스트 작성

- same Plane ticket 재처리 시 identity reuse.
- same Milestone path 재처리 시 identity reuse.
- provider revision mismatch는 conflict.
- NomadCode actor update는 new authoring trigger가 아님.

#### 중간 검증

```bash
cd services/core && go test ./internal/roadmapsync ./internal/storage
```

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/migrations/00006_create_roadmap_sync_identities.sql` | API-1 |
| `services/core/queries/roadmap_sync_identities.sql` | API-1 |
| `services/core/internal/db/roadmap_sync_identities.sql.go` | API-1 |
| `services/core/internal/storage/roadmap_sync_identities.go` | API-1 |
| `services/core/internal/storage/roadmap_sync_identities_test.go` | API-1 |
| `services/core/migrations/00007_create_roadmap_sync_steps.sql` | API-2 |
| `services/core/queries/roadmap_sync_steps.sql` | API-2 |
| `services/core/internal/db/roadmap_sync_steps.sql.go` | API-2 |
| `services/core/internal/storage/roadmap_sync_steps.go` | API-2 |
| `services/core/internal/storage/roadmap_sync_steps_test.go` | API-2 |
| `services/core/internal/roadmapsync/retry.go` | API-3 |
| `services/core/internal/roadmapsync/retry_test.go` | API-3 |

## 최종 검증

```bash
cd services/core && go test ./...
git diff --check
```

Go test cache output is acceptable for unchanged packages; changed packages must appear in output or be rerun directly as 중간 검증.

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