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

# Plan - REVIEW_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`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과/계획 대비 변경 사항에 기록한다.

## 배경

사용자 결정으로 code-server와 함께 올라간 Postgres를 사용하기로 하면서 `USER_REVIEW`의 환경 차단은 해소됐다. 실제 `GITO_TEST_DATABASE_URL="$DATABASE_URL"` 실행 결과 `revision_cursors.repo_id`의 FK 때문에 watch cursor 저장이 실패했다. branch watch/cursor 경로는 provider repo id를 source of truth로 쓰므로 repo registry 선등록 없이 동작해야 한다.

## 사용자 리뷰 요청 흐름

구현 중 차단 사유는 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/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-task/m-forgejo-branch-event-mvp/02+01_watch_store/user_review_0.log`
- `agent-task/m-forgejo-branch-event-mvp/02+01_watch_store/code_review_cloud_G07_2.log`
- `agent-task/m-forgejo-branch-event-mvp/02+01_watch_store/plan_cloud_G07_2.log`
- `services/core/internal/storage/postgres_test.go`
- `services/core/internal/storage/postgres.go`
- `services/core/internal/storage/storage.go`
- `services/core/internal/storage/storage_test.go`
- `services/core/internal/controlplane/runtime.go`
- `services/core/internal/controlplane/router.go`
- `services/core/migrations/00001_initial.sql`

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md`와 `agent-test/local/core-smoke.md`를 읽었다.
- core 변경 필수 검증은 `cd services/core && go test ./...`이며, storage behavior 변경은 해당 동작 테스트를 실행해야 한다.
- 이번 재개 결정에 따라 raw DSN을 기록하지 않고 `GITO_TEST_DATABASE_URL="$DATABASE_URL"` 형태로 code-server 동반 Postgres를 사용한다.
- 사전 실행 결과: `(cd services/core && GITO_TEST_DATABASE_URL="$DATABASE_URL" go test -v -count=1 ./internal/storage)`는 skip 없이 실행됐지만 `revision_cursors_repo_id_fkey` 위반으로 실패했다.

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

- 기존 fake/unit tests는 FK 제약이 있는 실제 Postgres schema와 runtime cursor write를 검증하지 못했다.
- 기존 Postgres integration test는 공유 DB public schema에 의존할 수 있어 이전 schema 상태의 영향을 받는다. per-test schema 격리가 필요하다.

### 심볼 참조

- renamed/removed symbol은 없다.
- `revision_cursors` 참조: `services/core/migrations/00001_initial.sql`, `services/core/internal/storage/postgres.go`, `services/core/internal/storage/postgres_test.go`, `services/core/internal/storage/storage_test.go`.

### 분할 판단

- split decision policy를 검토했다.
- 이번 변경은 기존 `02+01_watch_store` subtask의 Postgres evidence recovery 안에 있는 단일 storage schema/test 보정이다.
- 새 sibling split은 불필요하다. FK 제거와 integration test 격리는 같은 실패를 재현하고 검증하는 한 단위이며, 분리하면 DB evidence가 다시 흩어진다.
- predecessor `01_consumer_smoke`는 기존 archive complete로 충족된 상태에서 이어간다.

### 범위 결정 근거

- PgStore의 전체 `RepoStore` 구현은 제외한다. 현재 실패는 watch/cursor provider repo id와 `repos` registry FK의 잘못된 결합 때문이다.
- 새로운 Docker/compose baseline 추가는 제외한다. 사용자가 제공한 code-server Postgres DSN으로 검증한다.
- production migration rollout/alter migration 추가는 제외한다. 현재 MVP 초기 migration 파일을 조정하고 isolated schema test로 검증한다.

### 빌드 등급

- build lane: `cloud-G07`, review lane: `cloud-G07`.
- 실제 Postgres integration failure, schema/storage contract, prior USER_REVIEW 해소 후 follow-up이라 cloud-G07을 유지한다.

## 구현 체크리스트

- [ ] [REVIEW_REVIEW_API-1] `revision_cursors.repo_id`가 provider repo id를 저장할 수 있도록 초기 migration의 FK 결합을 제거한다.
- [ ] [REVIEW_REVIEW_API-2] Postgres integration test를 per-test schema로 격리하고 code-server Postgres에서 skip 없이 PASS하는 증거를 남긴다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REVIEW_API-1] Revision Cursor FK Decoupling

#### 문제

`services/core/migrations/00001_initial.sql:50`-`56`은 `revision_cursors.repo_id`에 `REFERENCES repos(id)`를 둔다. 그러나 runtime은 `services/core/internal/controlplane/runtime.go:220`-`229`에서 Forgejo push의 provider repo id로 cursor를 upsert하고, branch watch 등록도 `repos` row를 만들지 않는다. 실제 Postgres 검증은 `revision_cursors_repo_id_fkey`로 실패한다.

#### 해결 방법

초기 migration에서 `revision_cursors.repo_id`를 독립 text key로 둔다.

Before:

```sql
CREATE TABLE IF NOT EXISTS revision_cursors (
    repo_id      TEXT NOT NULL REFERENCES repos(id),
```

After:

```sql
CREATE TABLE IF NOT EXISTS revision_cursors (
    repo_id      TEXT NOT NULL,
```

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

- [ ] `services/core/migrations/00001_initial.sql`에서 `revision_cursors.repo_id` FK를 제거한다.
- [ ] `services/core/internal/storage/storage_test.go`의 migration smoke가 revision cursor schema 의도를 확인하도록 보완한다.

#### 테스트 작성

- migration smoke assertion을 보완한다.
- Postgres integration test는 `REVIEW_REVIEW_API-2`에서 실제 DB round-trip으로 검증한다.

#### 중간 검증

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

기대 결과: `GITO_TEST_DATABASE_URL`이 없는 기본 unit/smoke 테스트는 skip 포함 PASS한다.

### [REVIEW_REVIEW_API-2] Isolated Postgres Evidence

#### 문제

`services/core/internal/storage/postgres_test.go:13`-`135`는 외부 DB가 제공되면 public/default schema에 직접 migration을 적용하고 test row를 남긴다. code-server 동반 DB처럼 기존 schema가 이미 있는 환경에서는 이전 FK 상태의 영향을 받을 수 있고, 공유 DB 오염도 생긴다.

#### 해결 방법

테스트 시작 시 `GITO_TEST_DATABASE_URL`로 admin pool을 열어 unique schema를 만들고, test DSN에는 `search_path=<schema>` runtime parameter를 붙인다. `t.Cleanup`에서 `DROP SCHEMA ... CASCADE`로 정리한다. migration은 isolated schema에만 적용한다.

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

- [ ] `services/core/internal/storage/postgres_test.go`에 schema 생성/정리 helper를 추가한다.
- [ ] URL DSN에는 query parameter로 `search_path`를 추가하고, keyword DSN이면 안전하게 runtime parameter를 붙인다.
- [ ] test output에는 raw DSN을 기록하지 않는다.

#### 테스트 작성

- 기존 `TestPostgresBranchEventStorePersistsWatchCursorAndDedupesDelivery`를 격리 방식으로 강화한다.
- 새 별도 테스트는 만들지 않는다. 실패한 end-to-end storage behavior를 같은 테스트가 직접 검증한다.

#### 중간 검증

```bash
(cd services/core && GITO_TEST_DATABASE_URL="$DATABASE_URL" go test -v -count=1 ./internal/storage)
```

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

## 수정 파일 요약

| 파일 | 항목 |
| --- | --- |
| `services/core/migrations/00001_initial.sql` | REVIEW_REVIEW_API-1 |
| `services/core/internal/storage/storage_test.go` | REVIEW_REVIEW_API-1 |
| `services/core/internal/storage/postgres_test.go` | REVIEW_REVIEW_API-2 |
| `agent-task/m-forgejo-branch-event-mvp/02+01_watch_store/CODE_REVIEW-cloud-G07.md` | 구현 결과 기록 |

## 최종 검증

```bash
test -n "${DATABASE_URL:-}"
(cd services/core && GITO_TEST_DATABASE_URL="$DATABASE_URL" go test -v -count=1 ./internal/storage)
(cd services/core && go test -count=1 ./internal/controlplane ./internal/storage)
(cd services/core && go test -count=1 ./...)
(cd services/core && go vet ./...)
./bin/build
git diff --check
```

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