<!-- task=m-provider-adapter-foundation/01_webhook_adapter_contract plan=0 tag=API -->

# Provider Webhook Adapter Contract Plan

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

이 계획의 구현 완료는 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용, 검증 출력, 계획 대비 변경 사항으로 채운 뒤에만 성립한다. 구현 중 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 막으면 review stub의 `사용자 리뷰 요청` 섹션을 채우고 멈춘다. 직접 사용자에게 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경, secret, 서비스 준비, 검증 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과나 후속 plan 사유로 기록한다.

## 배경

현 마일스톤의 `webhook` 에픽에서 generic callback 경계는 작은 조각으로 추가되었지만, adapter별 signature 검증과 provider-neutral normalization은 아직 Forgejo 함수 호출에 묶여 있다. 먼저 provider 패키지에 webhook request, verification hook, normalization candidate 계약을 둬야 다음 wiring 작업이 기존 Forgejo path를 안전하게 갈아탈 수 있다.

## 사용자 리뷰 요청 흐름

사용자 리뷰 요청은 선택된 SDD 결정 또는 선택된 Milestone lock 결정이 실구현을 차단할 때만 active `CODE_REVIEW-*-G??.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 채팅 질문은 금지이며, code-review가 검증 후 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/provider-change-request-integration/milestones/provider-adapter-foundation.md`
- `agent-contract/index.md`
- `agent-contract/provided/gito-forgejo-branch-events-v1.md`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `services/core/go.mod`
- `services/core/internal/provider/provider.go`
- `services/core/internal/provider/provider_test.go`
- `services/core/internal/provider/forgejo/push.go`
- `services/core/internal/provider/forgejo/push_test.go`
- `services/core/internal/controlplane/router.go`
- `services/core/internal/controlplane/router_test.go`
- `services/core/internal/controlplane/runtime.go`
- `services/core/internal/controlplane/runtime_test.go`
- `services/core/internal/events/events.go`
- `services/core/internal/core/model.go`
- `services/core/internal/storage/storage.go`
- `services/core/internal/config/config.go`

### 테스트 환경 규칙

- 선택 env: `local`.
- `agent-test/local/rules.md` 읽음. core 변경은 `agent-test/local/core-smoke.md`로 라우팅된다.
- 적용 명령: `cd services/core && go test ./...`.
- 보조 fresh 명령: `cd services/core && go test -count=1 ./internal/provider ./internal/provider/forgejo ./internal/controlplane`.
- 누락 또는 `<확인 필요>` 값: 없음.

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

- provider webhook contract: 현재 `provider.WebhookEvent`는 payload container뿐이라 verifier/normalizer interface를 검증하는 테스트가 없다. 이번 plan에서 fake adapter compile-time/interface 테스트를 추가한다.
- Forgejo signature: `forgejo.VerifySignature` 함수 테스트는 있으나 adapter hook으로 검증되는지 확인하지 않는다. 후속 wiring plan에서 닫는다.
- Forgejo normalization: `forgejo.NormalizePush` 함수 테스트는 있으나 provider-neutral candidate 계약으로 변환되는지 확인하지 않는다. 후속 wiring plan에서 닫는다.

### 심볼 참조

- renamed/removed symbol: none.
- 참조 확인: `WebhookEvent`, `VerifySignature`, `NormalizePush`, `ParsePushPayload`, `handleProviderWebhook`, `handleForgejoPush`, `HandleProviderWebhook`를 `rg --sort path`로 확인했다.

### 분할 판단

- split decision policy를 먼저 평가했다.
- 공유 API/foundation 변경과 router/Forgejo call-site rollout이 분리되므로 split gate가 적용된다.
- shared task group: `agent-task/m-provider-adapter-foundation/`.
- `01_webhook_adapter_contract`: provider package contract만 정의한다. predecessor 없음.
- `02+01_webhook_adapter_wiring`: 이 contract의 `complete.log` 이후 Forgejo/router wiring을 수행한다.

### 범위 결정 근거

- 이 plan은 `services/core/internal/provider/**`의 interface와 contract tests만 대상으로 한다.
- `services/core/internal/controlplane/router.go` wiring, `/callbacks/forgejo/push` behavior migration, roadmap checkbox 갱신은 제외한다. 해당 변경은 dependent plan `02+01_webhook_adapter_wiring`에서 수행한다.
- 새 외부 dependency는 추가하지 않는다. `services/core/go.mod`에 필요한 표준 라이브러리만 사용 가능하다.

### 빌드 등급

- `local-G05`: 내부 Go API foundation이며 파일 수와 영향은 작지만 후속 router wiring의 계약이 되므로 중간 수준의 정확도가 필요하다.

## 구현 체크리스트

- [ ] provider webhook request, verification hook, normalization candidate, adapter interface를 `services/core/internal/provider/provider.go`에 추가한다.
- [ ] fake adapter 기반 provider package 테스트를 추가해 interface 구현, empty payload 보존, candidate type/revision 표현을 검증한다.
- [ ] `cd services/core && go test -count=1 ./internal/provider`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [API-1] Provider Webhook Contract

### 문제

- [services/core/internal/provider/provider.go](/config/workspace/gito/services/core/internal/provider/provider.go:61)는 `WebhookEvent`에 provider/event/delivery/payload만 담고 있어 signature 검증에 필요한 header/query/received time을 전달할 수 없다.
- [services/core/internal/provider/forgejo/push.go](/config/workspace/gito/services/core/internal/provider/forgejo/push.go:69)와 [services/core/internal/provider/forgejo/push.go](/config/workspace/gito/services/core/internal/provider/forgejo/push.go:81)는 verifier/normalizer hook 없이 직접 함수로만 노출된다.

Before:

```go
// provider.go:61
type WebhookEvent struct {
	Provider   string
	EventType  string
	ExternalID string
	Payload    []byte
}
```

After:

```go
type WebhookRequest struct {
	Provider   ProviderID
	EventType  string
	ExternalID string
	Payload    []byte
	Headers    map[string][]string
	Query      map[string][]string
	ReceivedAt time.Time
}

type WebhookCandidate struct {
	Type       string
	Provider   ProviderID
	ExternalID string
	Revision   *core.RevisionEvent
}

type WebhookVerifier interface {
	VerifyWebhook(ctx context.Context, req WebhookRequest) error
}

type WebhookNormalizer interface {
	NormalizeWebhook(ctx context.Context, req WebhookRequest) ([]WebhookCandidate, error)
}

type WebhookAdapter interface {
	WebhookVerifier
	WebhookNormalizer
}
```

### 해결 방법

- `WebhookEvent`를 `WebhookRequest`로 대체하거나 type alias로 호환 shim을 둔다. 이미 추가된 `controlplane.HandleProviderWebhook` call site가 있으므로 가능하면 한 번에 `WebhookRequest`로 정리한다.
- `WebhookCandidate.Type`은 `events.BranchUpdated` 같은 normalized event type 문자열을 담되 provider package가 events package에 의존하지 않게 string으로 유지한다.
- `Headers`와 `Query`는 `net/http` 타입을 provider package에 직접 노출하지 않기 위해 `map[string][]string`으로 둔다.

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

- [ ] `services/core/internal/provider/provider.go`: `WebhookRequest`, `WebhookCandidate`, `WebhookVerifier`, `WebhookNormalizer`, `WebhookAdapter` 추가.
- [ ] `services/core/internal/provider/provider_test.go`: fake adapter compile-time assertion과 request/candidate 보존 테스트 추가.
- [ ] `services/core/internal/controlplane/runtime.go`: `provider.WebhookEvent` 참조가 있으면 `provider.WebhookRequest`로 갱신.
- [ ] `services/core/internal/controlplane/router.go`: generic callback의 request 생성 타입만 갱신하고 동작은 바꾸지 않는다.

### 테스트 작성

- 작성: `services/core/internal/provider/provider_test.go`에 `TestWebhookAdapterContract`를 추가한다.
- 목표: fake adapter가 `WebhookAdapter`를 구현하고, request의 provider/event/delivery/payload/header/query가 verifier와 normalizer로 전달되며, candidate가 raw secret을 요구하지 않는 최소 metadata만 표현할 수 있음을 확인한다.

### 중간 검증

```bash
cd services/core && go test -count=1 ./internal/provider
```

기대 결과: exit 0.

## 수정 파일 요약

| 파일 | 항목 |
| --- | --- |
| `services/core/internal/provider/provider.go` | API-1 |
| `services/core/internal/provider/provider_test.go` | API-1 |
| `services/core/internal/controlplane/runtime.go` | API-1 |
| `services/core/internal/controlplane/router.go` | API-1 |

## 최종 검증

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

기대 결과: exit 0. Go test cache는 이 plan의 중간 확인에는 허용하지 않으므로 `-count=1`을 사용한다.

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