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

# Plan - REVIEW_REVIEW_API

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

`CODE_REVIEW-local-G05.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 필수다. 구현 후 검증을 실행하고 실제 stdout/stderr를 붙인 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 최종 판정, 로그 rename, `complete.log`, archive 이동은 code-review-skill 전용이다. 선택된 SDD 결정 또는 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 막으면 review stub의 `사용자 리뷰 요청` 섹션에 연결 대상, 근거, 실행 명령, 재개 조건을 기록하고 멈춘다. 구현 중 사용자에게 직접 묻거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경/secret/service 차단, 일반 범위 변경, 검증 공백은 사용자 리뷰가 아니라 검증 결과나 일반 follow-up으로 남긴다.

## 배경

이전 follow-up은 HMAC, `secret_ref` unsigned 차단, webhook failure retry rollback을 대부분 닫았다. 남은 문제는 provider delivery id가 없는 path에서 fallback consumer delivery id가 새 `EventRecord.ID`에 기대어 retry 간 바뀔 수 있다는 점이다. 이 follow-up은 fallback id를 revision identity에서 결정적으로 파생하고, 빈 provider delivery id에서 실패 후 재시도해도 header/body delivery id가 안정적인지 검증한다.

## 사용자 리뷰 요청 흐름

구현 중 직접 사용자에게 묻지 않는다. 선택된 SDD 결정 또는 Milestone lock 결정만 active review stub의 `사용자 리뷰 요청` 섹션에 기록하고, code-review가 검증해 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/provider-change-request-integration/milestones/webhook-revision-reconcile.md`
- Task ids:
  - `deliver`: `branch.updated` 같은 normalized event를 signed HTTP POST로 전달한다.
  - `nomadcode-wakeup`: NomadCode dev consumer가 generic `branch.updated` HTTP webhook delivery를 wakeup으로 매핑할 수 있는 interop path를 검증한다.
- Completion mode: check-on-pass

## Spec Targets

- SDD: `agent-roadmap/sdd/provider-change-request-integration/webhook-revision-reconcile/SDD.md`
- Acceptance scenarios:
  - `S02`: task=`deliver`; evidence=`cd services/core && go test -run TestWebhookDeliveryBranchUpdated ./internal/controlplane/ -v -count=1`
  - `S03`: task=`nomadcode-wakeup`; evidence=`fake NomadCode consumer test receives generic branch.updated without NomadCode-only required fields`
- Completion mode: spec-check-on-pass

## Archive Evidence Snapshot

- Current archived plan: `agent-task/m-webhook-revision-reconcile/01_http_delivery/plan_cloud_G06_1.log`
- Current archived review: `agent-task/m-webhook-revision-reconcile/01_http_delivery/code_review_cloud_G06_1.log`
- Prior loop history:
  - `agent-task/m-webhook-revision-reconcile/01_http_delivery/plan_local_G05_0.log` + `agent-task/m-webhook-revision-reconcile/01_http_delivery/code_review_local_G05_0.log` - FAIL; original HMAC/secret_ref/fallback/retry Required set.
  - `agent-task/m-webhook-revision-reconcile/01_http_delivery/plan_cloud_G06_1.log` + `agent-task/m-webhook-revision-reconcile/01_http_delivery/code_review_cloud_G06_1.log` - FAIL; fallback id is non-empty but not stable across empty-provider retry.
- Verdict: FAIL
- Required summary:
  - `services/core/internal/controlplane/runtime.go:535`: fallback `X-Gito-Delivery` uses random `record.ID` when provider delivery id is empty, so the same revision retry can get different consumer delivery ids.
- Suggested summary: 없음
- Nit summary: 없음
- Affected files:
  - `services/core/internal/controlplane/runtime.go`
  - `services/core/internal/controlplane/runtime_test.go`
- Verification evidence:
  - `go test -run TestWebhookDeliveryBranchUpdated ./internal/controlplane/ -v -count=1` from `services/core` - PASS.
  - `go test -run 'TestWebhookDeliveryBranchUpdated|TestRuntime_WebhookDeliveryWithSecret|TestRuntime_WebhookDeliveryUnsignedSecretRefFails|TestRuntime_WebhookDeliveryFallbackDeliveryID|TestRuntime_WebhookDeliveryFailureAllowsRetry' ./internal/controlplane/ -v -count=1` from `services/core` - PASS.
  - `go test ./...` from `services/core` - PASS.
  - `git diff --check` - PASS.
- Roadmap carryover: `deliver`, `nomadcode-wakeup`
- Spec carryover: `S02`, `S03`
- Narrow archive reread allowed: the two current archived log paths above only if details are needed.

## 분석 결과

### 읽은 파일

- `agent-contract/index.md`
- `agent-contract/provided/gito-forgejo-branch-events-v1.md`
- `agent-roadmap/sdd/provider-change-request-integration/webhook-revision-reconcile/SDD.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-test/local/core-smoke.md`
- `services/core/internal/controlplane/runtime.go`
- `services/core/internal/controlplane/runtime_test.go`
- `services/core/internal/controlplane/router.go`
- `services/core/internal/storage/storage.go`
- `services/core/internal/storage/postgres.go`

### 범위 결정 근거

- 이 follow-up은 consumer-facing fallback delivery id의 retry 안정성만 수정한다.
- HMAC, `secret_ref` unsigned 차단, store-backed failure rollback의 기존 수정은 유지한다.
- `/api/webhook-subscriptions` route shape, durable webhook delivery outbox/backoff table, provider callback response schema는 새로 만들지 않는다.
- Roadmap/SDD target은 그대로 유지한다. Required 수정이 완료되면 `deliver`와 `nomadcode-wakeup` PASS 후보가 된다.

### 라우팅

- build/review: `local-G05`
- 근거: 수정 범위가 `runtime.go`의 fallback helper와 `runtime_test.go`의 결정적 단위 테스트에 한정되고, 계약 요구는 로컬 테스트로 검증 가능하다.

## 구현 체크리스트

- [x] provider delivery id가 비어 있을 때 `consumerDeliveryID`가 random `record.ID` 대신 revision identity에서 결정적으로 파생한 stable non-empty id를 반환하도록 고친다.
- [x] header `X-Gito-Delivery`와 payload `delivery_id`가 같은 stable consumer delivery id를 쓰는지 유지한다.
- [x] 빈 provider delivery id에서 첫 webhook delivery가 5xx로 실패하고 같은 revision을 재시도할 때 두 요청의 `X-Gito-Delivery`와 body `delivery_id`가 모두 동일한지 검증하는 테스트를 추가하거나 기존 retry 테스트를 확장한다.
- [x] `cd services/core && go test -run 'TestRuntime_WebhookDeliveryFallbackDeliveryID|TestRuntime_WebhookDeliveryFailureAllowsRetry' ./internal/controlplane/ -v -count=1`를 실행한다.
- [x] `cd services/core && go test -run TestWebhookDeliveryBranchUpdated ./internal/controlplane/ -v -count=1`를 실행한다.
- [x] `cd services/core && go test ./...`를 실행한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [REVIEW_REVIEW_API-1] Stable Fallback Delivery ID Across Empty-Provider Retry

### 문제

`services/core/internal/controlplane/runtime.go:535`의 fallback은 provider delivery id가 없을 때 `record.ID`를 consumer delivery id로 사용한다. webhook delivery가 실패하면 provider delivery dedupe record가 rollback되고, 같은 revision retry에서 새 `EventRecord`가 만들어져 `record.ID`도 바뀐다. 이 경우 consumer가 `X-Gito-Delivery`로 retry idempotency를 유지할 수 없다.

### 해결 방법

provider delivery id가 없으면 `dedupeKeyFor("", record.Revision)` 또는 그 값을 안정적으로 hash한 문자열처럼 revision identity에서 결정적으로 파생한 id를 사용한다. header와 payload는 같은 helper 결과를 공유한다. consumer-facing id는 비어 있으면 안 되며, 같은 repo/branch/before/after revision identity에 대해 retry 간 동일해야 한다.

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

- [x] `services/core/internal/controlplane/runtime.go`: `consumerDeliveryID` fallback을 deterministic revision identity 기반으로 변경한다.
- [x] `services/core/internal/controlplane/runtime_test.go`: 빈 provider delivery id + 실패 후 재시도 테스트를 추가/확장해 두 요청의 `X-Gito-Delivery`와 body `delivery_id` 동일성을 검증한다.

### 테스트 작성

`HandleRevision(..., deliveryID="", revision)`으로 첫 delivery를 500 실패시키고 같은 revision으로 재시도한다. fake consumer는 실패 요청과 성공 요청을 모두 기록해야 하며, 두 요청의 `X-Gito-Delivery`가 같고 각 body `delivery_id`도 같은 값인지 확인한다. 기존 `TestRuntime_WebhookDeliveryFallbackDeliveryID`는 단일 요청의 non-empty 검증도 계속 유지한다.

### 중간 검증

```bash
cd services/core && go test -run 'TestRuntime_WebhookDeliveryFallbackDeliveryID|TestRuntime_WebhookDeliveryFailureAllowsRetry' ./internal/controlplane/ -v -count=1
```

## 수정 파일 요약

| 파일 | 항목 |
| --- | --- |
| `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 -run 'TestRuntime_WebhookDeliveryFallbackDeliveryID|TestRuntime_WebhookDeliveryFailureAllowsRetry' ./internal/controlplane/ -v -count=1
cd services/core && go test -run TestWebhookDeliveryBranchUpdated ./internal/controlplane/ -v -count=1
cd services/core && go test ./...
```

Expected: all commands exit 0, and empty provider delivery id retry keeps the same stable consumer delivery id across attempts.

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