<!-- task=m-plane-work-item-webhook-intake/03+02_idempotency plan=0 tag=WEBHOOK_IDEMPOTENCY -->

# Code Review Reference - WEBHOOK_IDEMPOTENCY

> **[IMPLEMENTING AGENT - READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation.
> Finalization is review-agent-only.

## 개요

date=2026-06-14
task=m-plane-work-item-webhook-intake/03+02_idempotency, plan=0, tag=WEBHOOK_IDEMPOTENCY

## Roadmap Targets

- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/plane-work-item-webhook-intake.md`
- Task ids:
  - `loop-idempotency`: duplicate webhook, provider retry, NomadCode self actor event를 중복 creation task 없이 처리한다.
- Completion mode: check-on-pass

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

> **[REVIEW AGENT ONLY]** 구현 결과를 실제 소스와 대조하고, PASS이면 code-review 절차에 따라 log/complete/archive를 처리한다. 구현 에이전트는 이 섹션을 실행하지 않는다.

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [WEBHOOK_IDEMPOTENCY-1] Storage Idempotency Boundary | [x] |
| [WEBHOOK_IDEMPOTENCY-2] Workflow And Pipeline Semantics | [x] |
| [WEBHOOK_IDEMPOTENCY-3] Webhook Duplicate/Self Tests | [x] |

## 구현 체크리스트

- [x] Plane-origin task idempotency key를 external provider/id로 고정하고 DB unique/index 또는 upsert semantics를 추가한다.
- [x] duplicate webhook/provider retry가 기존 task를 반환하거나 ignored 처리되어 새 task, workspace provision, slot reservation을 만들지 않도록 workflow/workitempipeline 경계를 조정한다. 검증: 같은 provider/work item/revision 재처리와 self actor 입력이 task 중복 생성으로 이어지지 않는다.
- [x] storage/workflow/workitempipeline/http tests를 추가해 duplicate external ref, duplicate webhook dispatch, self actor ignored를 검증한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

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

> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.

- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] active `CODE_REVIEW-*-G??.md`와 `PLAN-*-G??.md`를 `.log`로 아카이브한다.
- [ ] PASS이면 `complete.log`를 작성하고 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-plane-work-item-webhook-intake/03+02_idempotency/`로 이동한다.
- [ ] PASS이고 task group이 `m-plane-work-item-webhook-intake`이므로 완료 이벤트 메타데이터를 보고한다. roadmap 수정은 런타임 책임이다.

## 계획 대비 변경 사항

- `ReserveAvailableWorkspaceSlot` 쿼리(queries/workspace_slots.sql)에서 sqlc v1.31.1 파싱 시 `project_sync_setting_id` 모호성(ambiguity) 에러가 발생하여, 서브쿼리 테이블에 `ws` 에일리어스를 적용하여 수정하였습니다.

## 주요 설계 결정

- `workitempipeline.Service`에서 `CreateTaskFromWorkItem` 시작 지점에 `GetTaskByExternalRef`로 사전에 존재하는 task가 있는지 체크하도록 하여, workspace provision이나 slot reservation 등의 부가적인 side effect가 일어나기 전에 멱등하게 조기 반환하도록 설계했습니다.
- `queries/tasks.sql`의 `CreateTask` 쿼리에 `ON CONFLICT (external_provider, external_id) DO UPDATE SET updated_at = tasks.updated_at` 절을 추가하여 DB 레벨에서도 멱등성을 제공하도록 했습니다.
- `workflow.Service`에 `GetTaskByExternalRef`를 추가하고, input 파라미터(provider, id)의 validation 방어 코드를 작성했습니다.

## 사용자 리뷰 요청

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

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

- Duplicate prevention happens before workspace provision/slot reservation, or tests prove no duplicate side effect.
- DB unique/upsert semantics do not break tasks without external refs.
- Generated sqlc code matches `queries/tasks.sql`.

## 검증 결과

### WEBHOOK_IDEMPOTENCY-1 중간 검증

```bash
$ cd services/core && go run github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1 generate
(정상 완료)
```

### WEBHOOK_IDEMPOTENCY-2 중간 검증

```bash
$ cd services/core && go test -count=1 ./internal/workflow ./internal/workitempipeline
ok  	github.com/nomadcode/nomadcode-core/internal/workflow	0.003s
ok  	github.com/nomadcode/nomadcode-core/internal/workitempipeline	0.003s
```

### WEBHOOK_IDEMPOTENCY-3 중간 검증

```bash
$ cd services/core && go test -count=1 ./internal/http ./internal/workitempipeline
ok  	github.com/nomadcode/nomadcode-core/internal/http	0.004s
ok  	github.com/nomadcode/nomadcode-core/internal/workitempipeline	0.003s
```

### 최종 검증

```bash
$ cd services/core && go run github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1 generate
(정상 완료)
$ cd services/core && gofmt -w internal/db/tasks.sql.go internal/storage/store.go internal/workflow/service.go internal/workflow/service_test.go internal/workitempipeline/service.go internal/workitempipeline/service_test.go internal/http/plane_webhook_test.go
(정상 완료)
$ cd services/core && go test -count=1 ./internal/storage ./internal/workflow ./internal/workitempipeline ./internal/http
ok  	github.com/nomadcode/nomadcode-core/internal/storage	0.003s
ok  	github.com/nomadcode/nomadcode-core/internal/workflow	0.003s
ok  	github.com/nomadcode/nomadcode-core/internal/workitempipeline	0.003s
ok  	github.com/nomadcode/nomadcode-core/internal/http	0.009s
$ tar -C services/core -czf - . | ssh toki@toki-labs.com 'mkdir -p ~/agent-work/nomadcode/services/core && tar -C ~/agent-work/nomadcode/services/core -xzf -'
(정상 완료)
$ ssh toki@toki-labs.com 'zsh -lc "cd ~/agent-work/nomadcode/services/core && go test -count=1 ./..."'
ok  	github.com/nomadcode/nomadcode-core/cmd/plane-smoke	0.499s
?   	github.com/nomadcode/nomadcode-core/cmd/server	[no test files]
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/a2a	0.422s
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/jira	0.841s
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/mattermost	1.236s
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/openai	1.636s
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/plane	2.003s
?   	github.com/nomadcode/nomadcode-core/internal/agent	[no test files]
ok  	github.com/nomadcode/nomadcode-core/internal/authoring	2.342s
ok  	github.com/nomadcode/nomadcode-core/internal/config	2.682s
?   	github.com/nomadcode/nomadcode-core/internal/db	[no test files]
ok  	github.com/nomadcode/nomadcode-core/internal/gitoevents	3.095s
ok  	github.com/nomadcode/nomadcode-core/internal/gitosync	3.398s
ok  	github.com/nomadcode/nomadcode-core/internal/http	3.600s
?   	github.com/nomadcode/nomadcode-core/internal/model	[no test files]
ok  	github.com/nomadcode/nomadcode-core/internal/notification	3.928s
ok  	github.com/nomadcode/nomadcode-core/internal/projectsync	3.891s
ok  	github.com/nomadcode/nomadcode-core/internal/protosocket	3.957s
ok  	github.com/nomadcode/nomadcode-core/internal/roadmapsync	3.905s
ok  	github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline	3.885s
ok  	github.com/nomadcode/nomadcode-core/internal/scheduler	5.913s
ok  	github.com/nomadcode/nomadcode-core/internal/storage	3.947s
ok  	github.com/nomadcode/nomadcode-core/internal/workflow	3.908s
ok  	github.com/nomadcode/nomadcode-core/internal/workitem	3.873s
ok  	github.com/nomadcode/nomadcode-core/internal/workitempipeline	3.852s
```

## 코드리뷰 결과

- 종합 판정: FAIL
- 차원별 평가:
  - Correctness: Fail
  - Completeness: Fail
  - Test coverage: Fail
  - API contract: Pass
  - Code quality: Pass
  - Plan deviation: Fail
  - Verification trust: Pass
- 발견된 문제:
  - Required: `services/core/internal/workitempipeline/service.go:92`의 existing-task 선조회와 `services/core/internal/workitempipeline/service.go:127` 이후 workspace provision/slot reservation/CreateTask 흐름이 같은 DB transaction 또는 provider/id scoped lock으로 묶여 있지 않습니다. 동일 external ref webhook 2개가 동시에 들어오면 둘 다 `pgx.ErrNoRows`를 본 뒤 각각 slot을 예약하고, 마지막 `CreateTask`에서 한쪽만 upsert로 기존 task를 반환할 수 있어 두 번째 slot reservation side effect가 남습니다. 해결: external ref별 원자적 claim/lock을 side effect 전에 획득하거나, task row를 먼저 원자적으로 생성한 뒤 slot metadata를 갱신하는 구조로 바꾸고, 동시 duplicate 입력에서 `ReserveWorkspaceSlot`이 1회만 호출되는 회귀 테스트를 추가하세요.
  - Required: `services/core/queries/tasks.sql:4`와 `services/core/internal/storage/store.go:37`에서 storage/query 멱등성이 변경됐지만, `services/core/internal/storage`에는 `CreateTask` duplicate external ref와 `GetTaskByExternalRef`를 검증하는 task storage test가 없습니다. 또한 `services/core/internal/http/plane_webhook_test.go:526`의 duplicate webhook fake는 같은 task를 무조건 반환하고 `services/core/internal/http/plane_webhook_test.go:563`에서 creator 호출 2회만 확인하므로, 두 번째 dispatch가 duplicate side effect를 만들지 않는다는 계획 요구를 증명하지 못합니다. 해결: storage/query 레벨의 duplicate external ref 회귀 테스트와, handler 또는 pipeline 연동 테스트에서 두 번째 동일 webhook 처리 시 task/slot side effect가 추가되지 않음을 검증하세요.
- 다음 단계:
  - FAIL follow-up으로 `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성한다. USER_REVIEW 조건은 아니며, `complete.log`는 작성하지 않는다.
