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

# Provider Webhook Adapter Wiring Plan

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

이 계획은 `agent-task/m-provider-adapter-foundation/01_webhook_adapter_contract/complete.log`가 생긴 뒤 구현한다. 구현 완료는 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운 뒤에만 성립한다. 선택된 SDD 결정 또는 Milestone lock 결정이 막으면 review stub의 `사용자 리뷰 요청` 섹션을 채우고 멈춘다. 직접 사용자에게 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다.

## 배경

`/callbacks/forgejo/push`는 현재 config secret, Forgejo signature 함수, push parser, branch revision normalization을 라우터 안에서 직접 호출한다. provider adapter foundation의 남은 `verify`와 `normalize` task를 닫으려면 이 경계를 provider webhook adapter hook으로 옮기고, 기존 Forgejo contract 응답과 테스트를 유지해야 한다.

## 사용자 리뷰 요청 흐름

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

## Roadmap Targets

- Milestone: `agent-roadmap/phase/provider-change-request-integration/milestones/provider-adapter-foundation.md`
- Task ids:
  - `verify`: webhook signature 검증 hook을 adapter별로 꽂을 수 있게 한다.
  - `normalize`: provider payload를 provider-neutral event 후보로 변환한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `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`와 `agent-test/local/core-smoke.md`를 읽었다.
- 필수 검증: `cd services/core && go test ./...`.
- 이 plan의 fresh 중간 검증: `cd services/core && go test -count=1 ./internal/provider/forgejo ./internal/controlplane`.
- PostgreSQL이나 외부 provider 호출은 기본 local smoke에 포함하지 않는다.

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

- `verify`: 현재 `TestVerifySignature`와 `TestForgejoPushRequiresSignatureBeforeIgnoringUnsupportedEvent`는 있으나 adapter interface를 통한 검증 hook은 없다. 이 plan에서 Forgejo adapter hook 테스트와 router behavior test로 닫는다.
- `normalize`: 현재 `TestNormalizePushPayload`는 직접 함수만 검증한다. 이 plan에서 `NormalizeWebhook`이 `branch.updated` candidate를 만들고 tag/unsupported event는 side-effect 없는 후보 없음으로 처리하는 테스트를 추가한다.
- 기존 callback small work: `TestProviderCallbackRecordsReceivedWebhookEvent`가 fake provider receipt event를 검증한다. 이 plan은 그 endpoint의 raw payload non-leak 동작을 보존해야 한다.

### 심볼 참조

- renamed/removed symbol: none.
- call-site candidates:
  - `services/core/internal/controlplane/router.go:254` direct `forgejo.VerifySignature`.
  - `services/core/internal/controlplane/router.go:262` direct `forgejo.ParsePushPayload`.
  - `services/core/internal/controlplane/router.go:267` direct `forgejo.NormalizePush`.
  - `services/core/internal/provider/forgejo/push_test.go:89` direct signature unit test.
  - `services/core/internal/provider/forgejo/push_test.go:14` direct normalization unit test.

### 분할 판단

- split decision policy를 먼저 평가했다.
- 이 subtask는 `02+01_webhook_adapter_wiring`이며 predecessor `01_webhook_adapter_contract`에 의존한다.
- predecessor 상태: 같은 task group의 active sibling plan `agent-task/m-provider-adapter-foundation/01_webhook_adapter_contract/PLAN-local-G05.md`가 존재하지만 `complete.log`는 아직 없다. 구현은 해당 `complete.log`가 생긴 뒤 시작한다.
- API foundation과 router/Forgejo rollout이 분리되어 있어 이 dependent plan이 적절하다.

### 범위 결정 근거

- 이 plan은 Forgejo adapter hook과 existing callback wiring만 다룬다.
- GitHub/GitLab/Gitea/Plane/Jira 실제 provider 구현, PR/MR 생성, checks/comment API, durable provider webhook receipt store는 범위 제외다.
- `/callbacks/providers/fake` receipt endpoint는 callback task에서 추가된 동작을 보존하되, adapter registry가 없는 provider는 receipt-only로 남긴다.

### 빌드 등급

- `local-G06`: HTTP callback behavior와 existing Forgejo contract를 유지하면서 adapter hook으로 재배선하므로 bounded local work지만 회귀 위험이 있다.

## 구현 체크리스트

- [ ] `agent-task/m-provider-adapter-foundation/01_webhook_adapter_contract/complete.log`가 있는지 확인하고 없으면 구현하지 않고 dependency 대기 상태를 기록한다.
- [ ] Forgejo webhook adapter가 provider contract의 verifier/normalizer를 구현하도록 `services/core/internal/provider/forgejo/push.go`와 테스트를 갱신한다.
- [ ] `/callbacks/forgejo/push`가 adapter verification hook과 normalization candidate를 통해 기존 `branch.updated` event를 발행하도록 `services/core/internal/controlplane/router.go`와 테스트를 갱신한다.
- [ ] generic `/callbacks/providers/fake` receipt-only behavior와 raw payload non-leak 테스트가 계속 통과하는지 확인한다.
- [ ] `cd services/core && go test -count=1 ./internal/provider/forgejo ./internal/controlplane`와 `cd services/core && go test ./...`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

1. `01_webhook_adapter_contract` PASS 후 `complete.log` 확인.
2. Forgejo adapter hook 구현.
3. Router wiring 교체.
4. Focused tests 후 전체 core tests.

## [API-1] Forgejo Webhook Adapter

### 문제

- [services/core/internal/provider/forgejo/push.go](/config/workspace/gito/services/core/internal/provider/forgejo/push.go:69)는 signature 검증 함수만 제공해 adapter별 hook으로 꽂을 수 없다.
- [services/core/internal/provider/forgejo/push.go](/config/workspace/gito/services/core/internal/provider/forgejo/push.go:81)는 `core.RevisionEvent`를 직접 반환해 provider-neutral candidate 목록을 표현하지 못한다.

Before:

```go
// push.go:69
func VerifySignature(secret string, body []byte, signature string) bool { ... }

// push.go:81
func NormalizePush(payload PushPayload, repoID string, observedAt time.Time) (core.RevisionEvent, error) { ... }
```

After:

```go
type WebhookAdapter struct {
	Secret string
}

func (a WebhookAdapter) VerifyWebhook(ctx context.Context, req provider.WebhookRequest) error { ... }
func (a WebhookAdapter) NormalizeWebhook(ctx context.Context, req provider.WebhookRequest) ([]provider.WebhookCandidate, error) { ... }
```

### 해결 방법

- `VerifySignature`, `ParsePushPayload`, `NormalizePush`는 기존 tests와 call-site 안정성을 위해 유지한다.
- `WebhookAdapter.VerifyWebhook`은 secret이 비어 있으면 nil, secret이 있으면 Forgejo signature header를 검증하고 실패 시 sentinel error를 반환한다.
- `WebhookAdapter.NormalizeWebhook`은 event type이 비어 있거나 `push`이면 payload를 parse하고 branch ref만 `events.BranchUpdated` candidate로 반환한다. unsupported event와 non-branch ref는 empty candidate slice로 처리한다.

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

- [ ] `services/core/internal/provider/forgejo/push.go`: adapter type과 methods 추가.
- [ ] `services/core/internal/provider/forgejo/push_test.go`: compile-time interface assertion, signature hook success/fail, push normalization candidate, unsupported event empty candidate, tag ref empty candidate 테스트 추가.

### 테스트 작성

- 작성: `TestWebhookAdapterVerifiesSignature`, `TestWebhookAdapterNormalizesPushCandidate`, `TestWebhookAdapterIgnoresUnsupportedEvent`.
- 목표: adapter hook이 기존 Forgejo helper와 같은 semantic을 갖고 raw secret을 candidate에 넣지 않는지 확인한다.

### 중간 검증

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

기대 결과: exit 0.

## [API-2] Router Adapter Wiring

### 문제

- [services/core/internal/controlplane/router.go](/config/workspace/gito/services/core/internal/controlplane/router.go:253)는 라우터가 config secret과 Forgejo signature 함수를 직접 안다.
- [services/core/internal/controlplane/router.go](/config/workspace/gito/services/core/internal/controlplane/router.go:262)는 라우터가 Forgejo payload parse와 revision normalization을 직접 호출한다.

Before:

```go
// router.go:253
if strings.TrimSpace(cfg.ForgejoWebhookSecret) != "" &&
	!forgejo.VerifySignature(cfg.ForgejoWebhookSecret, body, r.Header.Get("X-Forgejo-Signature")) {
	writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid forgejo signature"})
	return
}
payload, err := forgejo.ParsePushPayload(body)
...
revision, err := forgejo.NormalizePush(payload, r.URL.Query().Get("repo_id"), time.Now().UTC())
```

After:

```go
adapter := forgejo.WebhookAdapter{Secret: cfg.ForgejoWebhookSecret}
req := provider.WebhookRequest{Provider: provider.ProviderForgejo, EventType: webhookEventName(r), ExternalID: r.Header.Get("X-Forgejo-Delivery"), Payload: body, Headers: headerMap(r.Header), Query: queryMap(r.URL.Query()), ReceivedAt: time.Now().UTC()}
if err := adapter.VerifyWebhook(r.Context(), req); err != nil { ...401... }
candidates, err := adapter.NormalizeWebhook(r.Context(), req)
```

### 해결 방법

- `handleForgejoPush`에서 request metadata를 `provider.WebhookRequest`로 구성한다.
- adapter verification error는 기존 contract처럼 `401`로 응답한다.
- candidate가 없으면 기존 unsupported/non-branch path처럼 `202 accepted, matched=false`로 응답한다.
- candidate type이 `branch.updated`이면 `runtime.HandleRevision`을 호출하고 기존 response shape을 유지한다.
- generic provider callback receipt-only route는 그대로 둔다.

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

- [ ] `services/core/internal/controlplane/router.go`: Forgejo direct helper calls를 adapter call로 교체한다.
- [ ] `services/core/internal/controlplane/router_test.go`: 기존 Forgejo tests를 유지하고 adapter path가 signature-before-ignore, watched branch, unwatched branch, fake receipt non-leak을 모두 만족하는지 확인한다.

### 테스트 작성

- 작성: 기존 router tests 보강. 새 test가 필요하면 `TestForgejoPushUsesAdapterNormalization`처럼 branch.updated candidate path를 검증한다.
- 목표: `/callbacks/forgejo/push` 응답 shape과 status가 contract와 동일하고, `/callbacks/providers/fake`는 adapter 없이 `provider.webhook.received`만 남긴다.

### 중간 검증

```bash
cd services/core && go test -count=1 ./internal/controlplane -run 'TestForgejoPush|TestProviderCallbackRecordsReceivedWebhookEvent'
```

기대 결과: exit 0.

## 수정 파일 요약

| 파일 | 항목 |
| --- | --- |
| `services/core/internal/provider/forgejo/push.go` | API-1 |
| `services/core/internal/provider/forgejo/push_test.go` | API-1 |
| `services/core/internal/controlplane/router.go` | API-2 |
| `services/core/internal/controlplane/router_test.go` | API-2 |

## 최종 검증

```bash
test -f agent-task/m-provider-adapter-foundation/01_webhook_adapter_contract/complete.log
cd services/core && go test -count=1 ./internal/provider/forgejo ./internal/controlplane
cd services/core && go test ./...
```

기대 결과: 첫 명령 exit 0, 두 Go test 명령 모두 exit 0. 전체 `go test ./...`는 repository baseline smoke이며 Go test cache 허용 가능하다.

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