<!-- task=m-webhook-revision-reconcile plan=2 tag=REVIEW_REVIEW_API -->

# Plan - REVIEW_REVIEW_API

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

`CODE_REVIEW-local-G05.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 필수 마지막 단계다. 구현 후 active 파일은 그대로 두고 리뷰 준비 상태를 보고한다. 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 막으면 review stub의 `사용자 리뷰 요청`에 정확한 근거를 남기고 멈춘다. 직접 사용자에게 묻거나, 선택지를 채팅으로 제시하거나, `USER_REVIEW.md`, archive log, `complete.log`를 만들지 않는다.

## Archive Evidence Snapshot

- Archived plan: `agent-task/m-webhook-revision-reconcile/plan_local_G05_1.log`
- Archived review: `agent-task/m-webhook-revision-reconcile/code_review_local_G05_1.log`
- Previous loop: `agent-task/m-webhook-revision-reconcile/plan_local_G05_0.log`, `agent-task/m-webhook-revision-reconcile/code_review_local_G05_0.log`
- Verdict: FAIL
- Required summary:
  - `ApplyReconcile` still records `reconcile:<idempotencyKey>` before `HandleRevision` completes side effects.
  - Required test `TestRuntimeApplyReconcileRetryAfterPublishFailure` is missing; `go test ./internal/controlplane -run TestRuntimeApplyReconcileRetryAfterPublishFailure -count=1` returned `[no tests to run]`.
  - The active review stub was left unfilled, so verification evidence was not recorded.
- Affected files: `services/core/internal/controlplane/runtime.go`, `services/core/internal/controlplane/runtime_test.go`, `agent-task/m-webhook-revision-reconcile/CODE_REVIEW-local-G05.md`
- Verification evidence: existing focused tests compile, but they do not cover first apply publish failure followed by retry with the same idempotency key.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/provider-change-request-integration/milestones/webhook-revision-reconcile.md`
- Task ids:
  - `expected-rev`: expected revision이 맞지 않으면 conflict로 멈춘다.
  - `dry-run`: reconcile dry-run 결과로 drift/conflict/proposal을 반환한다.
  - `apply`: idempotency key로 중복 적용 없이 change를 반영한다.
- Completion mode: check-on-pass

## Spec Targets

- SDD: `agent-roadmap/sdd/provider-change-request-integration/webhook-revision-reconcile/SDD.md`
- Acceptance scenarios:
  - `S08`: task=`expected-rev`; evidence=`expected revision mismatch test`
  - `S09`: task=`dry-run`; evidence=`reconcile dry-run test with drift/conflict/proposal output`
  - `S10`: task=`apply`; evidence=`idempotent apply test with repeated event/key and retry after failed publish`
- Completion mode: spec-check-on-pass

## 배경

이전 follow-up이 실행되지 않아 같은 Required 결함이 남아 있다. 현재 `ApplyReconcile`은 apply dedupe record를 먼저 저장한 뒤 `HandleRevision`을 호출한다. `HandleRevision`이 webhook 또는 broadcast 단계에서 실패하면 자기 delivery dedupe만 rollback하고, 선점된 apply dedupe는 남아 같은 key 재시도를 duplicate로 막을 수 있다.

## 구현 체크리스트

- [ ] `ApplyReconcile`에서 apply idempotency key가 side effect 성공 전에 영구 소비되지 않도록 수정한다.
- [ ] 실패한 첫 apply 뒤 같은 idempotency key 재시도가 duplicate가 아니라 적용 재시도로 진행되게 한다.
- [ ] `TestRuntimeApplyReconcileRetryAfterPublishFailure`를 추가하고 통과시킨다.
- [ ] 기존 `TestRuntimeApplyReconcileIsIdempotent`의 성공 후 duplicate 보장을 유지한다.
- [ ] 최종 검증 `cd services/core && go test ./internal/controlplane -run 'TestRuntimeReconcileExpectedRevisionConflict|TestRuntimeDryRunReconcileReportsDriftConflictProposal|TestRuntimeApplyReconcileIsIdempotent|TestRuntimeApplyReconcileRetryAfterPublishFailure' -count=1`, `cd services/core && go test ./...`, `git diff --check`를 실행한다.
- [ ] `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

### [REVIEW_REVIEW_API-1] Apply idempotency retry contract 수정

#### 문제

[runtime.go](/config/workspace/gito/services/core/internal/controlplane/runtime.go:1140)의 선점 apply dedupe record가 실제 side effect보다 먼저 기록된다. [runtime.go](/config/workspace/gito/services/core/internal/controlplane/runtime.go:1177)의 `HandleRevision`이 실패하면 선점 record는 지워지지 않고, 재시도는 [runtime.go](/config/workspace/gito/services/core/internal/controlplane/runtime.go:1158)에서 duplicate로 멈출 수 있다.

#### 해결 방법

권장 경로는 `ApplyReconcile`의 별도 선점 `RecordOnce`를 제거하고, `HandleRevision(ctx, provider, "reconcile:"+IdempotencyKey, revision)`의 기존 durable dedupe 결과를 apply idempotency로 사용하는 것이다. 반환된 `EventRecord.Duplicate`가 true이면 `ReconcileStatusDuplicate`, 첫 성공이면 `ReconcileStatusApplied`로 변환한다. 이렇게 하면 `HandleRevision`의 기존 rollback 정책이 실패 후 재시도를 허용한다.

별도 apply dedupe record를 유지한다면 `HandleRevision` error, unmatched/no-apply, webhook failure, broadcast failure 경로에서 선점 record를 반드시 rollback한다.

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

- [ ] `services/core/internal/controlplane/runtime.go`: `ApplyReconcile` idempotency 처리 수정.
- [ ] `services/core/internal/controlplane/runtime.go`: duplicate 결과가 성공한 기존 apply에만 대응하도록 반환 조건 정리.
- [ ] `services/core/internal/controlplane/runtime_test.go`: 실패 후 재시도 회귀 테스트 추가.

#### 테스트 작성

- 작성: `services/core/internal/controlplane/runtime_test.go`.
- 테스트명: `TestRuntimeApplyReconcileRetryAfterPublishFailure`.
- assertion:
  - 같은 store/watch/request를 사용한다.
  - 첫 runtime은 `fakeBroadcaster{err: ...}` 또는 동등한 실패 publisher로 `ApplyReconcile`을 실행해 conflict/error result를 받는다.
  - 같은 idempotency key로 성공 broadcaster runtime에서 재시도하면 `Status=applied`, `Applied=true`, `Duplicate=false`가 된다.
  - 성공 재시도 후 event/envelope/cursor가 적용 결과를 반영한다.
  - 이후 같은 key를 한 번 더 호출하면 `Status=duplicate`이고 side effect count가 늘지 않는다.

#### 중간 검증

```bash
cd services/core && go test ./internal/controlplane -run TestRuntimeApplyReconcileRetryAfterPublishFailure -count=1
```

## 의존 관계 및 구현 순서

1. `ApplyReconcile` dedupe 흐름을 수정한다.
2. 실패 후 재시도 테스트를 추가한다.
3. 기존 S08/S09/S10 focused tests와 전체 core tests를 재실행한다.
4. active `CODE_REVIEW-local-G05.md`의 구현 에이전트 소유 섹션을 실제 출력으로 채운다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/controlplane/runtime.go` | REVIEW_REVIEW_API-1 |
| `services/core/internal/controlplane/runtime_test.go` | REVIEW_REVIEW_API-1 |

## 최종 검증

```bash
cd services/core && go test ./internal/controlplane -run 'TestRuntimeReconcileExpectedRevisionConflict|TestRuntimeDryRunReconcileReportsDriftConflictProposal|TestRuntimeApplyReconcileIsIdempotent|TestRuntimeApplyReconcileRetryAfterPublishFailure' -count=1
cd services/core && go test ./...
git diff --check
```

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