<!-- task=m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency plan=2 tag=REVIEW_REVIEW_API -->

# PLAN local-G06 - REVIEW_REVIEW_API

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

이 작업은 직전 code-review FAIL의 Required issue만 좁게 해결한다. 구현 후 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채워야 완료된다. 구현 에이전트는 검증을 실행하고 active 파일을 유지한 채 리뷰 준비를 보고한다. 종결, 로그 rename, `complete.log`, task directory archive는 code-review 스킬 전용이다. 선택된 SDD 결정이나 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 막을 때만 review stub의 `사용자 리뷰 요청` 섹션을 채우고 멈춘다. 구현 중 사용자에게 직접 묻거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md`
- Task ids:
  - `receiver`: Gito `branch.updated` HTTP callback endpoint를 추가한다.
  - `idempotency`: `X-Gito-Delivery`와 revision key 기준으로 duplicate delivery를 중복 enqueue 없이 처리한다.
- Completion mode: check-on-pass

## Spec Targets

- SDD: `agent-roadmap/sdd/agent-ops-mcp-control-plane/gito-http-webhook-consumer-readiness/SDD.md`
- Acceptance scenarios:
  - `S01`: task=`receiver`; evidence=`HTTP handler unit/integration test, invalid signature test`
  - `S03`: task=`idempotency`; evidence=`duplicate delivery/revision idempotency test`
- Completion mode: spec-check-on-pass

## Archive Evidence Snapshot

- Archived plan: `agent-task/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/plan_local_G06_1.log`
- Archived review: `agent-task/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/code_review_local_G06_1.log`
- Verdict: FAIL
- Issue summary:
  - Required: `services/core/internal/http/gito_webhook.go:96` validates `changed_files` only as an array, not as a list of objects, so `{"changed_files":["not-an-object"]}` can still enter the accepted/handler path even though the SDD and `gitoevents.DecodeBranchUpdatedPayload` contract reject non-object items.
  - Suggested: 없음
  - Nit: 없음
- Affected files:
  - `services/core/internal/http/gito_webhook.go`
  - `services/core/internal/http/gito_webhook_test.go`
- Verification evidence:
  - `cd services/core && go test -count=1 ./internal/http ./internal/config ./cmd/server` - PASS.
  - `cd services/core && go test -count=1 ./...` - PASS.
  - Existing HTTP receiver tests are insufficient because they cover `changed_files` as a non-list string but not a list containing non-object elements.
- Roadmap/spec carryover:
  - Roadmap Targets remain `receiver` and `idempotency`.
  - Spec Targets remain S01 and S03. This follow-up directly repairs S01; S03 should remain green.
- Narrow reread allowed if needed:
  - `agent-task/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/plan_local_G06_1.log`
  - `agent-task/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/code_review_local_G06_1.log`

## 범위 결정 근거

- 포함: HTTP receiver `changed_files` item-type validation and the matching receiver test proving handler is not called.
- 제외: Gito outbound producer/subscription, scan/enqueue bridge behavior, config/docs, delivery/revision idempotency semantics, and unrelated roadmap/SDD edits.
- 새 package는 만들지 않는다. 기존 local validator 안에서 `gitoevents.decodeChangedFiles`의 item object check와 같은 의미를 최소 구현한다.

## 구현 체크리스트

- [ ] `validateBranchUpdatedPayload`가 `changed_files` present 시 배열 여부뿐 아니라 각 element가 JSON object인지 검증하고, non-object element를 400 reject하도록 보강한다.
- [ ] `changed_files` 배열 안의 non-object item이 400이고 handler를 호출하지 않음을 검증하는 HTTP receiver test를 추가하며 기존 wrong body type, malformed string, accepted/duplicate tests가 계속 통과하게 한다.
- [ ] `cd services/core && go test -count=1 ./internal/http`를 실행한다.
- [ ] `cd services/core && go test -count=1 ./internal/http ./internal/config ./cmd/server`를 실행한다.
- [ ] `cd services/core && go test -count=1 ./...`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [REVIEW_REVIEW_API-1] `changed_files` element contract validation

### 문제

현재 HTTP receiver validator는 `changed_files`가 JSON array인지까지만 확인한다. 하지만 SDD와 `gitoevents.DecodeBranchUpdatedPayload` 기준은 `changed_files`가 present이면 list of objects여야 한다. 따라서 아래 payload는 현재 HTTP path에서 invalid로 거부되어야 하지만 통과할 수 있다.

```json
{"repo_id":"target-repo","branch":"develop","changed_files":["not-an-object"]}
```

### 해결 방법

- `services/core/internal/http/gito_webhook.go`의 `validateBranchUpdatedPayload`에서 `p.ChangedFiles.([]interface{})`를 얻은 뒤 각 item이 `map[string]interface{}`인지 확인한다.
- non-object item 발견 시 `payload changed_files[<index>] is not an object`처럼 원인을 알 수 있는 error를 반환한다.
- 검증 실패는 기존 path처럼 HTTP 400과 handler call 없음으로 종료한다.
- `gitoevents` package import cycle을 만들지 않는다.

### 테스트 결정

- `services/core/internal/http/gito_webhook_test.go`에 `TestReceiveGitoWebhookRejectsMalformedChangedFileItemWithoutHandlerCall`을 추가한다.
- payload는 signed `branch.updated` 요청으로 만들고 `changed_files`에 string element를 넣는다.
- 기대값은 HTTP 400, fake handler call count 0이다.

### 중간 검증

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

예상: PASS. Go test cache output은 허용하지 않는다.

## 최종 검증

```bash
cd services/core && go test -count=1 ./internal/http ./internal/config ./cmd/server
cd services/core && go test -count=1 ./...
```

예상: 두 명령 모두 PASS. Go test cache output은 허용하지 않는다.
