<!-- task=m-forgejo-branch-event-mvp/02+01_watch_store plan=1 tag=REVIEW_API -->

# Plan - REVIEW_API

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

구현이 끝났다고 판단하기 전에 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용, 설계 결정, 검증 출력으로 채운다. 검증 명령을 실행하고 출력 원문을 붙인 뒤 active 파일을 그대로 둔 채 리뷰 준비를 보고한다. 최종 판정, log archive, `complete.log`, task directory archive 이동은 code-review-skill 전용이다.

구현 중 사용자만 결정할 수 있는 범위 변경, 사용자 소유 외부 환경/secret/service 준비, 또는 계획 충돌이 발생하면 active review stub의 `사용자 리뷰 요청` 섹션에 정확한 증거를 채우고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과/계획 대비 변경 사항에 기록한다.

## 배경

첫 번째 리뷰는 duplicate delivery suppression 자체는 대체로 구현됐지만, duplicate REST response가 첫 delivery의 event와 일관되지 않고 서버의 Postgres migration path가 루트 실행에서 깨질 수 있다고 판정했다. 또한 계획상 필수였던 Postgres-backed integration evidence가 Docker 부재로 skip되어 API-4 완료 근거가 부족하다. 이 follow-up은 기존 설계를 넓히지 않고, idempotent response 계약과 server wiring, 실제 DB 검증 증거만 보완한다.

## 사용자 리뷰 요청 흐름

구현 중 차단 사유는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`에서 복사한 형식을 사용하며, 구현 에이전트는 직접 사용자 prompt를 띄우지 않는다. code-review가 요청의 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/control-plane-foundation/milestones/forgejo-branch-event-mvp.md`
- Task ids:
  - `watch-store`: branch watch와 revision cursor를 Postgres store로 옮긴다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `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/contracts/rules.md`
- `agent-ops/rules/project/domain/workspace-ops/rules.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-ops/skills/common/plan/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/contracts-smoke.md`
- `agent-test/local/workspace-ops-smoke.md`
- `agent-roadmap/current.md`
- `agent-contract/index.md`
- `agent-contract/provided/gito-forgejo-branch-events-v1.md`
- `agent-task/m-forgejo-branch-event-mvp/02+01_watch_store/plan_cloud_G07_0.log`
- `agent-task/m-forgejo-branch-event-mvp/02+01_watch_store/code_review_cloud_G07_0.log`
- `agent-task/archive/2026/06/m-forgejo-branch-event-mvp/01_consumer_smoke/complete.log`
- `bin/build`
- `bin/test`
- `bin/lint`
- `services/core/cmd/server/main.go`
- `services/core/go.mod`
- `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/core/model.go`
- `services/core/internal/provider/forgejo/push.go`
- `services/core/internal/provider/forgejo/push_test.go`
- `services/core/internal/protosocket/server.go`
- `services/core/internal/protosocket/subscription.go`
- `services/core/internal/protosocket/subscription_test.go`
- `services/core/internal/storage/storage.go`
- `services/core/internal/storage/storage_test.go`
- `services/core/internal/storage/postgres.go`
- `services/core/internal/storage/postgres_test.go`
- `services/core/migrations/00001_initial.sql`

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md`를 읽었다. local 기본은 standard remote runner이며, 현재 checkout의 `git diff --check`는 편집 직후 preflight로만 사용한다.
- 매칭 profile은 `agent-test/local/core-smoke.md`, `agent-test/local/contracts-smoke.md`, `agent-test/local/workspace-ops-smoke.md`다.
- core 필수 검증은 `cd services/core && go test ./...`이며, storage/provider/proto-socket behavior 변경은 해당 focused test 또는 차단 사유가 필요하다.
- contracts 변경은 `git diff --check`와 `agent-contract/index.md` pointer 일관성 확인이 필요하다.
- workspace-op artifact 변경은 Agent-Ops gitignore block과 task artifact ignore 여부 확인이 필요하다.
- 리뷰 중 재실행한 명령: `cd services/core && go test -count=1 ./internal/controlplane`, `cd services/core && go test -count=1 ./internal/storage`, `cd services/core && go test -count=1 ./internal/provider/forgejo ./internal/protosocket`, `cd services/core && go test -count=1 ./...`, `cd services/core && go vet ./...`, `git diff --check`. 모두 PASS했다. `command -v docker`는 exit 1이었다.

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

- duplicate delivery가 `/api/events` count를 늘리지 않는지는 검증하지만, duplicate response의 `event.id`/`created_at`이 첫 delivery response와 동일한지는 검증하지 않는다.
- `DATABASE_URL` 설정 시 server가 루트 cwd에서 migration SQL을 찾을 수 있는지 검증하지 않는다.
- `TestPostgresBranchEventStorePersistsWatchCursorAndDedupesDelivery`는 작성됐지만 실제 `GITO_TEST_DATABASE_URL`을 가진 PostgreSQL against run evidence가 없다.

### 심볼 참조

- renamed/removed symbol은 없다.
- 변경 대상 주요 호출부:
  - `controlplane.NewRouterWithStore`: `services/core/cmd/server/main.go`, `services/core/internal/controlplane/runtime_test.go`
  - `Runtime.HandleRevision`: `services/core/internal/controlplane/router.go`, `services/core/internal/controlplane/runtime_test.go`
  - `ProviderDeliveryStore.RecordOnce`: `services/core/internal/controlplane/runtime.go`, `services/core/internal/storage/postgres.go`, fake stores
  - `storage.NewPgStore`: `services/core/cmd/server/main.go`, `services/core/internal/storage/postgres_test.go`

### 분할 판단

기존 task group은 `m-forgejo-branch-event-mvp`이고 현재 subtask는 `02+01_watch_store`다. predecessor `01_consumer_smoke`는 `agent-task/archive/2026/06/m-forgejo-branch-event-mvp/01_consumer_smoke/complete.log`로 충족됐다. 이번 follow-up은 같은 subtask의 review fix이며, 별도 sibling으로 분할하면 같은 runtime/store 계약을 중복 조정하게 되어 현재 디렉터리에서 이어간다.

### 범위 결정 근거

- persistent event outbox 전체 구현은 여전히 범위 제외다. 필요한 경우 provider delivery ledger가 duplicate response에 필요한 최소 event metadata를 반환하거나 조회하도록 좁게 확장한다.
- Redis fanout, PR/MR provider API, NomadCode Plane mutation은 범위 제외다.
- 기존 REST/proto-socket event payload schema는 breaking change 없이 유지한다.
- Docker helper나 repository-wide compose baseline 추가는 범위 제외다. 검증은 disposable Postgres container 또는 이미 준비된 `GITO_TEST_DATABASE_URL`로 수행한다.

### 빌드 등급

build lane은 `cloud-G07`, review lane은 `cloud-G07`이다. storage/migration, REST idempotency contract, server startup path, 실제 Postgres evidence가 함께 움직이는 verification-trust follow-up이다.

## 구현 체크리스트

- [ ] [REVIEW_API-1] duplicate delivery response가 첫 delivery의 event와 일관된 idempotent response를 반환하도록 store/runtime/router/tests를 보완한다.
- [ ] [REVIEW_API-2] `DATABASE_URL` 설정 서버가 루트 실행에서도 migration SQL을 안정적으로 적용하도록 migration loading 경로를 수정하고 검증한다.
- [ ] [REVIEW_API-3] 실제 PostgreSQL against integration test와 focused/full 검증을 실행해 stdout/stderr evidence를 남긴다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_API-1] Duplicate Response Event Consistency

#### 문제

`services/core/internal/controlplane/runtime.go:154`에서 매 push마다 새 `EventRecord`를 만든 뒤 duplicate path인 `runtime.go:194`-`214`가 새 record에 `Duplicate=true`만 붙여 반환한다. 이 record는 `/api/events`에 저장되지 않고 broadcast되지도 않아 REST response의 `event`가 실제 첫 delivery event와 달라진다. `agent-contract/provided/gito-forgejo-branch-events-v1.md:86`-`94`는 duplicate response가 첫 delivery와 같은 event shape를 반환해야 한다.

#### 해결 방법

`ProviderDeliveryStore` contract를 확장해 duplicate 여부와 기존 delivery/event metadata를 함께 반환하거나, duplicate 판정 뒤 기존 delivery를 조회할 수 있는 method를 추가한다. 최소한 duplicate response의 `event.id`, `delivery_id`, `revision`, `created_at`이 첫 delivery response와 일치해야 한다. persistent event outbox 전체를 만들지 말고, provider delivery ledger가 현재 contract를 만족하는 데 필요한 최소 정보만 보유/조회한다.

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

- [ ] `services/core/internal/storage/storage.go`의 delivery store contract를 idempotent response에 필요한 형태로 조정한다.
- [ ] `services/core/internal/storage/postgres.go`와 fake stores를 새 contract에 맞춘다.
- [ ] `services/core/internal/controlplane/runtime.go`가 duplicate일 때 새 transient event를 response로 반환하지 않게 한다.
- [ ] `services/core/internal/controlplane/runtime_test.go` 또는 `router_test.go`에서 첫 번째/두 번째 duplicate response의 event id와 created_at 일관성을 검증한다.
- [ ] `agent-contract/provided/gito-forgejo-branch-events-v1.md` 문구가 실제 behavior와 일치하는지 확인하되, breaking change로 바꾸지 않는다.

#### 중간 검증

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

기대 결과: duplicate delivery response 일관성 테스트와 storage contract tests가 PASS한다.

### [REVIEW_API-2] Server Migration Path

#### 문제

`services/core/cmd/server/main.go:21`은 `storage.NewPgStore(..., "migrations/00001_initial.sql")`를 사용한다. `bin/build:8`은 루트 `bin/gito-server`를 생성하므로 루트 cwd에서 `DATABASE_URL`을 설정하고 서버를 실행하면 `services/core/migrations/00001_initial.sql`을 찾지 못할 수 있다.

#### 해결 방법

권장 해결은 migration SQL을 `cmd/server` 또는 storage package에서 `//go:embed`로 포함하고 `NewPgStore`가 SQL content 또는 migration source를 받을 수 있게 하는 것이다. 대안으로는 config/환경변수로 migration path를 명시하고 기본값을 루트와 `services/core` cwd 모두에서 해석 가능한 경로 후보로 처리한다. 어느 방법을 택하든 루트 cwd에서 `DATABASE_URL`이 설정된 startup path가 migration path 오류로 실패하지 않게 한다.

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

- [ ] `services/core/cmd/server/main.go` 또는 `services/core/internal/storage/postgres.go`에서 migration loading을 cwd 의존 없이 만든다.
- [ ] 필요한 경우 `services/core/internal/config/config.go`에 migration path/env를 추가하되 기존 no-DB 실행은 유지한다.
- [ ] 루트 cwd 실행 또는 test helper로 migration source가 resolve되는지 검증한다.
- [ ] raw DSN, credential, private endpoint를 tracked 파일이나 로그에 남기지 않는다.

#### 중간 검증

```bash
(cd services/core && go test -count=1 ./cmd/server ./internal/storage)
./bin/build
```

기대 결과: server package/storage tests와 root build가 PASS한다.

### [REVIEW_API-3] Postgres Integration Evidence

#### 문제

첫 구현의 `CODE_REVIEW`는 API-4를 완료로 표시했지만, 실제 Postgres integration test는 Docker 부재로 skip됐다. Roadmap target의 source-of-truth 전환을 PASS하려면 fake store와 migration snippet만으로는 부족하다.

#### 해결 방법

코드 보완 후 disposable PostgreSQL container 또는 이미 준비된 `GITO_TEST_DATABASE_URL`로 `TestPostgresBranchEventStorePersistsWatchCursorAndDedupesDelivery`를 실제 실행한다. Docker가 여전히 없고 대체 `GITO_TEST_DATABASE_URL`도 없다면, 구현을 완료로 표시하지 말고 `사용자 리뷰 요청`에 `command -v docker`, 대체 DSN 부재, 재개 조건을 정확히 기록한다.

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

- [ ] `services/core/internal/storage/postgres_test.go`가 실제 PostgreSQL against run에서 deterministic하게 동작하는지 확인한다.
- [ ] duplicate response fix 이후 focused tests를 다시 실행한다.
- [ ] 최종 검증 출력에 실제 stdout/stderr를 붙인다.

#### 중간 검증

```bash
(cd services/core && GITO_TEST_DATABASE_URL='postgres://postgres:postgres@127.0.0.1:15460/gito_test?sslmode=disable' go test -count=1 ./internal/storage)
```

기대 결과: `TestPostgresBranchEventStorePersistsWatchCursorAndDedupesDelivery`가 skip 없이 PASS한다.

## 수정 파일 요약

| 파일 | 항목 |
| --- | --- |
| `services/core/internal/storage/storage.go` | REVIEW_API-1 |
| `services/core/internal/storage/postgres.go` | REVIEW_API-1, REVIEW_API-2 |
| `services/core/internal/storage/postgres_test.go` | REVIEW_API-1, REVIEW_API-3 |
| `services/core/internal/controlplane/runtime.go` | REVIEW_API-1 |
| `services/core/internal/controlplane/runtime_test.go` | REVIEW_API-1 |
| `services/core/internal/controlplane/router_test.go` | REVIEW_API-1 |
| `services/core/cmd/server/main.go` | REVIEW_API-2 |
| `services/core/internal/config/config.go` | REVIEW_API-2 |
| `agent-contract/provided/gito-forgejo-branch-events-v1.md` | REVIEW_API-1 |

## 최종 검증

```bash
command -v docker
docker rm -f gito-watch-store-test >/dev/null 2>&1 || true
docker run --name gito-watch-store-test -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=gito_test -p 15460:5432 -d postgres:16
for i in $(seq 1 30); do docker exec gito-watch-store-test pg_isready -U postgres && break; sleep 1; done
(cd services/core && GITO_TEST_DATABASE_URL='postgres://postgres:postgres@127.0.0.1:15460/gito_test?sslmode=disable' go test -count=1 ./internal/storage ./internal/controlplane ./internal/provider/forgejo ./internal/protosocket)
(cd services/core && go test -count=1 ./...)
(cd services/core && go vet ./...)
./bin/build
git diff --check
docker rm -f gito-watch-store-test
```

기대 결과: Docker availability check PASS, disposable PostgreSQL-backed focused tests PASS, full Go test PASS, go vet PASS, root build PASS, diff whitespace check PASS. 모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
