# SDD: Webhook Revision Reconcile

## 위치

- Milestone: `agent-roadmap/archive/phase/provider-change-request-integration/milestones/webhook-revision-reconcile.md`
- Phase: `agent-roadmap/phase/provider-change-request-integration/PHASE.md`

## 상태

[승인됨]

## SDD 잠금

- 상태: 해제
- 사용자 리뷰: 없음
- 잠금 항목:
  - 없음

## 문제 / 비목표

- 문제: NomadCode가 Gito의 `branch.updated` event를 generic HTTP webhook wakeup으로 받을 수 있어야 한다. 동시에 webhook delivery는 최종 source of truth가 아니며, delivery idempotency, retry, signature, revision 재검증 경계를 구현 전에 고정해야 한다.
- 비목표:
  - NomadCode 전용 필수 payload field를 base schema에 추가하지 않는다.
  - provider별 모든 webhook payload를 이 Milestone에서 지원하지 않는다.
  - webhook delivery 성공을 NomadCode 또는 다른 consumer의 상태 적용 완료 증거로 취급하지 않는다.
  - proto-socket, gRPC, Redis Streams fanout을 external consumer delivery 경로로 추가하지 않는다.
  - UI conflict resolution 화면은 이 Milestone에서 다루지 않는다.

## Source of Truth

| 영역 | 기준 | 메모 |
|------|------|------|
| Roadmap | `agent-roadmap/phase/provider-change-request-integration/milestones/webhook-revision-reconcile.md` | Milestone 목표, 범위, 기능 Task 완료 기준 |
| Code | `services/core/internal/controlplane/`, `services/core/internal/events/`, `services/core/internal/worker/` | REST subscription/delivery surface, normalized event, durable delivery/outbox 구현 기준 |
| External Provider | Forgejo push webhook, Gito outbound HTTP webhook consumer | Forgejo push는 wakeup source, consumer delivery는 `branch.updated` wakeup signal |
| User Decision | 없음 | 기존 `gito.forgejo-branch-events.v1` 계약을 따른다 |

## State Machine

| 상태 | 진입 조건 | 다음 상태 | 근거 |
|------|-----------|-----------|------|
| webhook-received | Forgejo push callback이 유효 payload로 수신된다 | event-recorded 또는 ignored | `POST /callbacks/forgejo/push`, watch match 여부 |
| ignored | event가 unsupported 또는 unwatched branch에 해당한다 | 없음 | response `accepted=true`, `matched=false` |
| event-recorded | watched branch의 before/after revision이 normalized `branch.updated`로 기록된다 | delivery-pending | durable event/outbox record |
| delivery-pending | matching webhook subscription이 존재한다 | delivery-sending | subscription event/repo/branch filter |
| delivery-sending | worker가 pending delivery를 HTTP POST로 전송한다 | delivery-succeeded 또는 delivery-retryable 또는 delivery-failed | target HTTP response, timeout, signature config |
| delivery-succeeded | consumer endpoint가 2xx를 반환한다 | 없음 | delivery attempt record |
| delivery-retryable | consumer endpoint가 429 또는 5xx를 반환하거나 transient network failure가 발생한다 | delivery-pending 또는 delivery-failed | retry/backoff policy |
| delivery-failed | consumer endpoint가 non-retryable response를 반환하거나 retry budget이 소진된다 | 없음 | delivery attempt state |
| reconcile-conflict | expected revision이 현재 source of truth와 맞지 않는다 | dry-run-result 또는 없음 | Git revision scan 또는 provider read API |
| applied | idempotency key로 중복 적용이 차단되고 change apply가 성공한다 | 없음 | apply result and idempotency record |

## Interface Contract

- 계약 원문: `agent-contract/provided/gito-forgejo-branch-events-v1.md`
- 입력:
  - Forgejo callback: `POST /callbacks/forgejo/push`
  - branch watch: `POST /api/listeners/branches`
  - webhook subscription: `POST /api/webhook-subscriptions`
  - subscription filter: `events`, `repo_id`, `branch`
  - signing reference: `secret_ref`
  - revision identity: `repo_id`, `branch`, `before`, `after`
- 출력:
  - normalized event: `branch.updated`
  - webhook headers: `X-Gito-Event`, `X-Gito-Delivery`, optional `X-Gito-Signature`
  - consumer payload: `id`, `type`, `provider`, `delivery_id`, `repo_id`, `branch`, `before`, `after`, `changed_files`, `observed_at`, `created_at`
  - delivery attempt state: pending, sending, succeeded, retryable, failed
- 금지:
  - raw secret, token, password, credential 값을 tracked docs, event payload, log에 남기지 않는다.
  - NomadCode 전용 필수 field를 base `branch.updated` payload에 추가하지 않는다.
  - provider callback이 NomadCode, Plane, roadmap source of truth를 직접 변경하지 않는다.
  - consumer webhook delivery를 최종 상태 증거로 취급하지 않는다.
  - external consumer delivery에 proto-socket을 요구하지 않는다.

## Acceptance Scenarios

| ID | Milestone Task | Given | When | Then |
|----|----------------|-------|------|------|
| S01 | `subscription` | consumer endpoint, event type, repo/branch filter, `secret_ref`가 입력된다 | webhook subscription을 등록한다 | subscription id와 filter가 저장되고 raw secret은 저장되지 않는다 |
| S02 | `deliver` | watched `nomadcode/develop` branch에서 `branch.updated` event가 기록되고 matching subscription이 있다 | delivery worker가 event를 처리한다 | fake consumer가 signed HTTP POST, delivery id, base payload를 수신한다 |
| S03 | `nomadcode-wakeup` | NomadCode dev consumer가 generic `branch.updated` endpoint를 제공한다 | Gito가 matching delivery를 전송한다 | consumer가 NomadCode 전용 필수 field 없이 wakeup으로 매핑할 수 있다 |
| S04 | `retry` | consumer가 429 또는 5xx를 반환한다 | delivery attempt가 기록된다 | retry/backoff 상태가 PostgreSQL 기준으로 남고 delivery idempotency가 유지된다 |
| S05 | `webhook-source` | Forgejo push callback이 들어온다 | payload가 watch와 match된다 | `provider.webhook.received` 또는 equivalent wakeup source event와 normalized branch event가 추적 가능하다 |
| S06 | `poll-source` | provider webhook이 누락되거나 지연된다 | polling fallback shape를 사용한다 | 같은 revision identity로 change source를 표현할 수 있다 |
| S07 | `git-source` | Git revision scan이 before/after revision을 알고 있다 | provider event와 scan result를 reconcile한다 | 동일 repo/branch/revision identity로 중복 이벤트를 피한다 |
| S08 | `expected-rev` | expected revision과 현재 branch revision이 다르다 | reconcile을 시도한다 | apply 없이 conflict로 멈춘다 |
| S09 | `dry-run` | event와 현재 Git/provider 상태가 주어진다 | dry-run을 실행한다 | drift, conflict, proposal이 관찰 가능한 결과로 반환된다 |
| S10 | `apply` | 같은 event 또는 idempotency key가 반복 처리된다 | apply를 두 번 요청한다 | side effect는 한 번만 실행되고 중복은 idempotent 결과로 수렴한다 |

## Evidence Map

| Scenario | Required Evidence | `agent-task` 연결 | `Spec Completion` 기대 |
|----------|-------------------|------------------|---------------------------|
| S01 | subscription create/list test, secret value grep | `agent-task/m-webhook-revision-reconcile/...` | `subscription` task가 contract filter와 `secret_ref` storage boundary를 만족했다 |
| S02 | `cd services/core && go test -run TestWebhookDeliveryBranchUpdated ./internal/controlplane/ -v -count=1` | `agent-task/m-webhook-revision-reconcile/...` | `deliver` task가 fake consumer delivery id, headers, payload 수신을 증명했다 |
| S03 | NomadCode interop smoke 또는 fake NomadCode consumer test | `agent-task/m-webhook-revision-reconcile/...` | `nomadcode-wakeup` task가 generic `branch.updated`만으로 wakeup mapping을 증명했다 |
| S04 | delivery retry/backoff unit or integration test with PostgreSQL store | `agent-task/m-webhook-revision-reconcile/...` | `retry` task가 retryable/non-retryable status와 delivery idempotency를 증명했다 |
| S05 | Forgejo callback test and event inspection evidence | `agent-task/m-webhook-revision-reconcile/...` | `webhook-source` task가 provider callback을 wakeup source로 저장/추적함을 증명했다 |
| S06 | polling fallback shape contract or unit test | `agent-task/m-webhook-revision-reconcile/...` | `poll-source` task가 polling source를 동일 change model로 표현함을 증명했다 |
| S07 | revision identity dedupe/reconcile test | `agent-task/m-webhook-revision-reconcile/...` | `git-source` task가 Git revision scan과 provider event identity 연결을 증명했다 |
| S08 | expected revision mismatch test | `agent-task/m-webhook-revision-reconcile/...` | `expected-rev` task가 mismatch conflict stop을 증명했다 |
| S09 | reconcile dry-run test with drift/conflict/proposal output | `agent-task/m-webhook-revision-reconcile/...` | `dry-run` task가 apply 없이 observable proposal 결과를 증명했다 |
| S10 | idempotent apply test with repeated event/key | `agent-task/m-webhook-revision-reconcile/...` | `apply` task가 repeated processing side effect once를 증명했다 |

## Cross-repo Dependencies

- NomadCode는 external consumer로 `branch.updated` delivery를 수신한다. 현재 SDD는 `.agent-roadmap-sync/locks.yaml` entry를 요구하지 않는다.
- 계약 기준은 `agent-contract/provided/gito-forgejo-branch-events-v1.md`이다.

<!-- 필요한 경우:
- `<project>:agent-roadmap/phase/<phase>/milestones/<milestone>.md`
- `.agent-roadmap-sync/locks.yaml` entry: `<id>`
-->

## Drift Check

- [x] Milestone 기능 Task와 Acceptance Scenario가 일치한다.
- [x] Evidence Map이 plan/code-review/complete.log에서 검증 가능하다.
- [x] agent-contract를 쓰는 경우 SDD에 계약 원문을 복제하지 않았다.
- [x] 사용자 리뷰가 필요한 항목은 `USER_REVIEW.md`에만 남겼다.

## 사용자 리뷰 이력

- 없음

## 작업 컨텍스트

- 표준선: webhook은 빠른 wakeup signal이며 최종 판정은 Git revision/diff 또는 provider read API로 수행한다. 외부 event delivery는 HTTP webhook/callback이고, proto-socket은 내부 control/runtime 규약으로만 남긴다.
- 후속 SDD: 없음
