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

# Code Review Reference - API

> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> Fill implementation-owned sections, run verification, keep active files in place, and report ready for review. Do not prompt the user directly during implementation.

## 개요

date=2026-06-13
task=m-milestone-work-item-creation-sync/05+03,04_idempotency_retry, plan=0, tag=API

## 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

## 이 파일을 읽는 리뷰 에이전트에게

> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다.

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [API-1] Identity Map Persistence | [x] |
| [API-2] Step Ledger | [x] |
| [API-3] Retry Decision Service | [x] |

## 구현 체크리스트

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

## 코드리뷰 전용 체크리스트

- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] active 파일을 `.log`로 아카이브하고 FAIL follow-up `PLAN-cloud-G07.md` / `CODE_REVIEW-cloud-G07.md`를 작성한다.
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임 완료 이벤트 메타데이터를 보고한다.

## 계획 대비 변경 사항

- 계획의 dependency 메모는 `03_develop_match`/`04_plane_todo_projection` complete.log가 missing이라고 적혀 있으나, 현재 두 predecessor 모두 `agent-task/archive/2026/06/m-milestone-work-item-creation-sync/03+02_develop_match/complete.log`와 `04+03_plane_todo_projection/complete.log`로 아카이브되어 있어 선행 의존성이 충족된 상태로 진행했다. (CLAUDE.md의 split subtask 선행 의존성 확인 예외에 따라 동일 task group의 후보 complete.log 존재만 확인)
- 계획은 identity 충돌(`conflict sentinel`)을 query 단계에서 처리하는 것처럼 표현했으나, `(provider, tenant, project, work_item_id)`와 `(roadmap_milestone_path)` 두 unique index는 단일 `ON CONFLICT` target으로 묶을 수 없다. 따라서 store 계층 `UpsertRoadmapSyncIdentity`에서 두 lookup을 선행 비교해 mismatched pair를 `ErrRoadmapSyncIdentityConflict`로 반환하도록 했다. raw unique-index 위반 대신 typed conflict로 막는다.
- API-3의 retry decision service는 계획대로 `internal/roadmapsync`에 두되, 기존 패키지가 `workitem`만 import하는 경계를 유지하기 위해 `db`/`storage`를 import하지 않는다. 호출자가 persisted identity(`ExistingIdentity`)와 completed step set(`map[Step]bool`)을 주입하는 provider-neutral 시그니처(`ReconcileCreationCycle`)로 설계했다.
- 함수명은 계획의 `ReconcileCreationCycle` 그대로 사용했다.

## 주요 설계 결정

- **이중 unique index로 양방향 중복 차단**: 같은 Plane 티켓(`provider+tenant+project+work_item_id`)과 같은 Milestone path(`roadmap_milestone_path`) 양쪽에 unique index를 두어, 어느 쪽으로 재처리해도 동일 identity row를 재사용한다. upsert는 work item key 기준 `ON CONFLICT ... DO UPDATE`이며, path가 다른 ticket에 이미 묶여 있거나 ticket이 다른 path에 묶여 있으면 conflict로 막는다.
- **step ledger의 idempotent upsert**: `(roadmap_sync_identity_id, step)` unique index + `ON CONFLICT DO UPDATE`(no-op self-assign)로 이미 완료한 step을 재마킹해도 원본 `completed_at`을 유지하는 no-op이 된다. step은 DB CHECK과 store `Valid()` 양쪽에서 closed set(`develop_matched`, `original_comment_preserved`, `plane_body_updated`, `plane_todo_moved`)으로 제한하고, unknown step은 DB를 건드리기 전에 `ErrUnknownRoadmapSyncStep`으로 거부한다.
- **resume 결정은 ordered step의 first-incomplete**: `ReconcileCreationCycle`은 completed set에 없는 첫 ordered step부터 재개(`ActionResume`)하고, 전부 완료면 `ActionComplete`를 반환한다. 성공한 step은 다시 호출되지 않는다.
- **actor guard**: trigger actor가 `SelfActor`(NomadCode self mutation)와 같으면 `ActionSkipSelfMutation`을 반환해 NomadCode가 직접 만든 body/title 변경이 새 authoring trigger로 오인되지 않게 한다.
- **revision mismatch는 silent overwrite 대신 conflict**: 이미 identity가 존재할 때 trigger의 provider/roadmap revision이 persisted 값과 다르면 `ActionConflict`를 반환한다. 단, 둘 중 하나가 빈 값(미기록)이면 첫 cycle을 막지 않도록 conflict로 보지 않는다.
- **generated db code 수동 작성**: 로컬에 sqlc 바이너리가 없어(`bin/migrate-up`은 goose만 `go run`으로 사용) 기존 `workspace_slots.sql.go` 등과 동일한 sqlc v1.31.1 스타일로 `*.sql.go`와 `models.go` 구조체를 손으로 작성했다. nullable `provider_revision`/`roadmap_revision`은 `sqlc.yaml`의 `emit_pointers_for_null_types`에 맞춰 `*string`으로 두었다.

## 사용자 리뷰 요청

- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음

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

- provider work item id와 Milestone path 양쪽 unique path가 중복 생성을 막는지 확인한다.
- completed step이 재시도에서 다시 호출되지 않는지 확인한다.
- actor guard가 NomadCode self mutation을 새 user trigger로 처리하지 않는지 확인한다.

## 검증 결과

### API-1 중간 검증
```bash
$ cd services/core && go test ./internal/storage ./internal/db
ok  	github.com/nomadcode/nomadcode-core/internal/storage	0.003s
?   	github.com/nomadcode/nomadcode-core/internal/db	[no test files]
```

### API-2 중간 검증
```bash
$ cd services/core && go test ./internal/storage
ok  	github.com/nomadcode/nomadcode-core/internal/storage	0.004s
```

### API-3 중간 검증
```bash
$ cd services/core && go test ./internal/roadmapsync ./internal/storage
ok  	github.com/nomadcode/nomadcode-core/internal/roadmapsync	0.004s
ok  	github.com/nomadcode/nomadcode-core/internal/storage	(cached)
```

### 최종 검증
```bash
$ cd services/core && go test ./...
ok  	github.com/nomadcode/nomadcode-core/cmd/plane-smoke	(cached)
?   	github.com/nomadcode/nomadcode-core/cmd/server	[no test files]
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/a2a	(cached)
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/jira	(cached)
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/mattermost	(cached)
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/openai	(cached)
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/plane	(cached)
?   	github.com/nomadcode/nomadcode-core/internal/agent	[no test files]
ok  	github.com/nomadcode/nomadcode-core/internal/authoring	0.009s
ok  	github.com/nomadcode/nomadcode-core/internal/config	(cached)
?   	github.com/nomadcode/nomadcode-core/internal/db	[no test files]
ok  	github.com/nomadcode/nomadcode-core/internal/http	0.004s
?   	github.com/nomadcode/nomadcode-core/internal/model	[no test files]
ok  	github.com/nomadcode/nomadcode-core/internal/notification	(cached)
ok  	github.com/nomadcode/nomadcode-core/internal/projectsync	(cached)
ok  	github.com/nomadcode/nomadcode-core/internal/protosocket	0.012s
ok  	github.com/nomadcode/nomadcode-core/internal/roadmapsync	(cached)
ok  	github.com/nomadcode/nomadcode-core/internal/scheduler	2.013s
ok  	github.com/nomadcode/nomadcode-core/internal/storage	(cached)
ok  	github.com/nomadcode/nomadcode-core/internal/workflow	0.004s
ok  	github.com/nomadcode/nomadcode-core/internal/workitem	(cached)
ok  	github.com/nomadcode/nomadcode-core/internal/workitempipeline	(cached)

$ git diff --check
(출력 없음, exit 0)

$ cd services/core && go vet ./internal/roadmapsync ./internal/storage ./internal/db && gofmt -l internal/roadmapsync internal/storage internal/db
(출력 없음, vet exit 0 / gofmt 차이 없음)
```

---

## 코드리뷰 결과

- 종합 판정: FAIL
- 차원별 평가:
  - correctness: Fail
  - completeness: Fail
  - test coverage: Fail
  - API contract: Fail
  - code quality: Pass
  - plan deviation: Fail
  - verification trust: Warn
- 발견된 문제:
  - Required: `services/core/queries/roadmap_sync_identities.sql:16`의 `ON CONFLICT (provider, tenant, project, work_item_id) DO UPDATE`가 기존 row의 `roadmap_milestone_path` 일치 조건 없이 path를 덮어쓴다. `services/core/internal/storage/roadmap_sync_identities.go:55`의 선조회는 순차 실행에서는 충돌을 잡지만 원자적이지 않다. 두 재처리가 같은 provider work item에 서로 다른 Milestone path로 동시에 들어오면 둘 다 선조회에서 no rows를 본 뒤, 나중 upsert가 기존 identity를 새 path로 조용히 재바인딩할 수 있다. 수정: upsert SQL에 `WHERE roadmap_sync_identities.roadmap_milestone_path = EXCLUDED.roadmap_milestone_path` 같은 조건부 update를 추가하고, 조건 불일치로 `RETURNING` row가 없거나 path unique 위반이 발생하면 `ErrRoadmapSyncIdentityConflict`로 변환한다. 이 경로를 단위 테스트로 고정한다.
  - Required: `services/core/internal/roadmapsync/identity.go:100`의 `Identity.Matches`가 `tenant`와 `project`를 비교하지 않아, `services/core/internal/roadmapsync/retry.go:117`의 retry decision이 같은 provider/work item id/path라도 다른 Plane workspace/project identity를 같은 identity로 재사용할 수 있다. 이번 plan과 DB unique key는 provider, tenant, project, work item id를 identity key로 저장하므로 retry decision도 동일한 키를 비교해야 한다. 수정: retry/develop matching에서 쓰는 identity match가 tenant/project까지 비교하도록 보완하거나, retry 전용 strict identity match를 도입하고 tenant/project mismatch 테스트를 추가한다.
  - Suggested: `./bin/sqlc` 재생성을 시도했지만 `queries/workspace_slots.sql:19:11: column reference "project_sync_setting_id" is ambiguous`로 실패해 신규 generated DB 파일이 실제 sqlc 산출물과 일치하는지 도구로 재검증하지 못했다. follow-up에서는 위 Required 수정 뒤 `go test`와 함께 이 생성 경로의 현재 차단 사유를 명시하거나, 가능한 경우 sqlc 재생성 증거를 남긴다.
- 다음 단계: FAIL follow-up plan/review를 생성해 Required 이슈를 수정하고 검증한다.
