<!-- task=m-gito-http-webhook-consumer-readiness/01_payload_boundary plan=0 tag=API -->

# PLAN local-G04 - API

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

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

## 배경

현 Milestone은 Gito `branch.updated`를 HTTP webhook JSON body로 받아야 한다. 현재 `gitoevents` decoder는 proto-socket envelope에 묶여 있어 HTTP receiver가 같은 base payload 계약을 재사용하기 어렵다. 이 선행 작업은 NomadCode 전용 필드 없이 Gito base payload를 decode하는 공통 경계를 만든다.

## 사용자 리뷰 요청 흐름

사용자 리뷰 요청은 선택된 SDD 결정 또는 선택된 Milestone lock 결정만 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식을 따른다. 구현 중 직접 사용자에게 질문하지 않으며, code-review가 정당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md`
- Task ids:
  - `payload-boundary`: Gito base payload를 repo/branch/revision/change hint 중심으로 해석하고 NomadCode 전용 필드를 요구하지 않는다.
- 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:
  - `S02`: task=`payload-boundary`; evidence=`payload decode/filter tests without NomadCode-specific fields`
- Completion mode: spec-check-on-pass

## 분석 결과

### 읽은 파일

- `AGENTS.md`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/private/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/workspace-ops/rules.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/roadmap-sdd/SKILL.md`
- `agent-ops/skills/common/update-roadmap/SKILL.md`
- `agent-ops/skills/common/check-roadmap-dependency/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-test/local/workspace-ops-smoke.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/PHASE.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md`
- `agent-roadmap/sdd/agent-ops-mcp-control-plane/gito-http-webhook-consumer-readiness/SDD.md`
- `../gito/agent-contract/provided/gito-forgejo-branch-events-v1.md`
- `services/core/go.mod`
- `services/core/internal/gitoevents/events.go`
- `services/core/internal/gitoevents/events_test.go`
- `services/core/internal/gitoevents/client.go`
- `services/core/internal/gitoevents/client_test.go`
- `services/core/internal/gitosync/bridge.go`
- `services/core/internal/gitosync/bridge_test.go`
- `services/core/internal/gitosync/scanner.go`
- `services/core/internal/http/router.go`
- `services/core/internal/http/handlers.go`
- `services/core/internal/http/handlers_test.go`
- `services/core/internal/http/plane_webhook.go`
- `services/core/internal/http/plane_webhook_test.go`
- `services/core/internal/config/config.go`
- `services/core/internal/config/config_test.go`
- `services/core/cmd/server/main.go`

### 테스트 환경 규칙

- 선택한 test_env: `local`.
- `agent-test/local/rules.md`를 읽었다.
- matching profile: `agent-test/local/core-smoke.md`를 읽었다.
- 적용 명령: core 변경은 최소 `cd services/core && go test ./...`; focused unit은 `cd services/core && go test -count=1 ./internal/gitoevents`.
- `core-smoke.md`의 기준 출력 예시는 `<확인 필요>`지만 필수 검증 명령은 구체적이다.
- dependency manifest는 `services/core/go.mod`를 확인했다. 새 package 추가는 필요 없다.

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

- 기존 `TestDecodeBranchUpdatedEnvelopeAcceptsConsumerNeutralBasePayload`는 proto-socket envelope 내부 payload만 검증한다.
- HTTP JSON body가 같은 base payload decoder를 직접 재사용하는지는 아직 테스트가 없다. 이 작업에서 `DecodeBranchUpdatedPayload` 테스트를 추가한다.
- changed_files malformed, missing repo_id/branch 테스트는 기존 envelope path에 있다. 새 payload decoder에도 최소 base payload, wrong type/action, malformed changed_files 회귀를 추가한다.

### 심볼 참조

- renamed/removed symbol: 없음.
- 새 symbol 후보: `DecodeBranchUpdatedPayload`.

### 분할 판단

- split decision policy를 먼저 평가했다.
- shared task group: `m-gito-http-webhook-consumer-readiness`.
- sibling subtasks:
  - `01_payload_boundary`: 독립 선행 작업. payload decoder foundation과 tests.
  - `02+01_http_receiver_idempotency`: `01_payload_boundary`의 `complete.log` 필요. HTTP receiver가 공통 decoder를 소비한다.
- API/foundation과 HTTP route/wiring/idempotency의 위험과 검증 전략이 달라 split한다.
- 이 subtask는 predecessor가 없는 독립 작업이다.

### 범위 결정 근거

- 포함: `services/core/internal/gitoevents/events.go`, `services/core/internal/gitoevents/events_test.go`.
- 제외: `services/core/internal/http/**`, `services/core/cmd/server/main.go`, `services/core/internal/config/**`, docs. HTTP route, signing, idempotency, config wiring은 `02+01_http_receiver_idempotency`가 담당한다.
- 제외: Gito 계약 원문 변경. SDD가 외부 계약 source of truth로 링크한다.

### 빌드 등급

- `local-G04`: 내부 decoder API와 focused tests에 한정된 bounded Go 변경이며, 검증은 deterministic unit + core test로 가능하다.

## 구현 체크리스트

- [x] `gitoevents`에 HTTP JSON body와 proto-socket envelope가 공유하는 `branch.updated` base payload decoder를 추가하고, `payload-boundary` 검증을 포함한다.
- [x] `cd services/core && go test -count=1 ./internal/gitoevents`를 실행한다.
- [x] `cd services/core && go test -count=1 ./...`를 실행한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] 공통 branch.updated payload decoder

#### 문제

`services/core/internal/gitoevents/events.go:76`의 `DecodeBranchUpdatedEnvelope`가 envelope channel/action 검증과 payload decode를 한 함수에 묶고 있다. HTTP receiver는 Gito contract의 JSON body를 envelope 없이 받으므로 같은 base payload를 재사용할 공통 decoder가 없다.

Before:

```go
// services/core/internal/gitoevents/events.go:76
// DecodeBranchUpdatedEnvelope validates the envelope channel/action and decodes
// its payload into a BranchUpdatedEvent. It rejects wrong channel/action and
// malformed payloads with a descriptive error instead of returning a partial
// event.
func DecodeBranchUpdatedEnvelope(env protosocket.Envelope) (BranchUpdatedEvent, error) {
	var ev BranchUpdatedEvent

	if env.Channel != EventChannel {
		return ev, fmt.Errorf("gitoevents: unexpected channel %q, want %q", env.Channel, EventChannel)
	}
	if env.Action != BranchUpdatedAction {
		return ev, fmt.Errorf("gitoevents: unexpected action %q, want %q", env.Action, BranchUpdatedAction)
	}
	if env.Payload == nil {
		return ev, fmt.Errorf("gitoevents: missing payload")
	}

	ev.ID = stringField(env.Payload, "id")
	ev.Type = stringField(env.Payload, "type")
	ev.Provider = stringField(env.Payload, "provider")
	ev.DeliveryID = stringField(env.Payload, "delivery_id")
	ev.RepoID = stringField(env.Payload, "repo_id")
	ev.Branch = stringField(env.Payload, "branch")
	ev.Before = stringField(env.Payload, "before")
	ev.After = stringField(env.Payload, "after")
	ev.ObservedAt = stringField(env.Payload, "observed_at")
	ev.CreatedAt = stringField(env.Payload, "created_at")
```

#### 해결 방법

- `DecodeBranchUpdatedPayload(payload map[string]any) (BranchUpdatedEvent, error)`를 추가한다.
- `DecodeBranchUpdatedEnvelope`는 channel/action/null 검증 후 `DecodeBranchUpdatedPayload(env.Payload)`만 호출한다.
- payload decoder는 `repo_id`와 `branch`를 필수로 유지하고, `type`이 비어 있지 않으면 `branch.updated`여야 한다.
- `provider`, `delivery_id`, timestamp, optional extension fields는 필수로 만들지 않는다.

After outline:

```go
func DecodeBranchUpdatedEnvelope(env protosocket.Envelope) (BranchUpdatedEvent, error) {
	if env.Channel != EventChannel {
		return BranchUpdatedEvent{}, fmt.Errorf("gitoevents: unexpected channel %q, want %q", env.Channel, EventChannel)
	}
	if env.Action != BranchUpdatedAction {
		return BranchUpdatedEvent{}, fmt.Errorf("gitoevents: unexpected action %q, want %q", env.Action, BranchUpdatedAction)
	}
	if env.Payload == nil {
		return BranchUpdatedEvent{}, fmt.Errorf("gitoevents: missing payload")
	}
	return DecodeBranchUpdatedPayload(env.Payload)
}

func DecodeBranchUpdatedPayload(payload map[string]any) (BranchUpdatedEvent, error) {
	// Fill BranchUpdatedEvent from base payload fields.
	// Reject missing repo_id/branch and non-branch.updated type.
}
```

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

- [x] `services/core/internal/gitoevents/events.go`: common payload decoder 추가, envelope decoder 위임.
- [x] `services/core/internal/gitoevents/events_test.go`: base payload without NomadCode fields, wrong type, malformed changed_files 테스트 추가.

#### 테스트 작성

- 작성한다.
- `services/core/internal/gitoevents/events_test.go`
  - `TestDecodeBranchUpdatedPayloadAcceptsBasePayloadWithoutNomadCodeFields`: Gito base JSON shape map에서 repo/branch/after/changed_files decode.
  - `TestDecodeBranchUpdatedPayloadRejectsWrongType`: `type != branch.updated` reject.
  - `TestDecodeBranchUpdatedPayloadRejectsMalformedChangedFiles`: changed_files malformed reject.

#### 중간 검증

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

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

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/gitoevents/events.go` | API-1 |
| `services/core/internal/gitoevents/events_test.go` | API-1 |

## 최종 검증

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

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

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