<!-- task=m-control-plane-edge-wire-baseline/01_cp_edge_endpoint plan=1 tag=REVIEW_EDGE_CONN -->

# Plan - REVIEW_EDGE_CONN Follow-up

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

이 plan은 `code_review_cloud_G08_0.log`의 FAIL 판정을 해소하는 좁은 후속 작업이다. 구현의 마지막 단계는 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 내용과 검증 출력으로 채우는 것이다. 사용자만 결정할 수 있는 범위 충돌, 외부 환경/secret 준비, 제품 결정이 필요하면 review stub의 `사용자 리뷰 요청`을 근거와 함께 채우고 멈춘다. 단순 증거 공백은 후속 에이전트가 재실행할 수 있으므로 사용자 리뷰 요청으로 올리지 않는다.

## 배경

첫 번째 리뷰에서 Control Plane Edge TCP endpoint 구현은 기본 동작 테스트를 통과했지만, registry reconnect correctness, container/compose 운영 표면, proto 변경 증거 신뢰성에서 Required 문제가 발견되었다. 이 후속 작업은 기존 subtask 범위 안에서 해당 문제만 수정한다.

## 구현 체크리스트

- [ ] [REVIEW_EDGE_CONN-1] Edge registry disconnect를 connection generation으로 보호하고 stale disconnect 테스트를 추가한다.
- [ ] [REVIEW_EDGE_CONN-2] Edge TCP wire endpoint를 Docker/compose/docs 운영 표면에 노출한다.
- [ ] [REVIEW_EDGE_CONN-3] proto 변경 범위와 생성물 검증 증거를 실제 diff와 일치하게 기록한다.
- [ ] 중간/최종 검증 명령을 실행하고 출력이 기대와 일치하는지 확인한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_EDGE_CONN-1] Registry generation guard

#### 문제

`EdgeRegistry`가 `edge_id`만으로 현재 연결 상태를 저장하고 `MarkDisconnected(edgeID, ...)`도 같은 키만 사용한다. 같은 Edge가 재연결하거나 중복 연결된 뒤 오래된 connection의 disconnect listener가 늦게 실행되면 새 연결 상태를 disconnected로 덮을 수 있다.

#### 해결 방법

connection별 generation/token을 도입한다. hello 처리 시 해당 connection token으로 `MarkConnected`를 기록하고, disconnect 시 같은 token이 현재 state의 token과 일치할 때만 `Connected=false`로 바꾼다. token은 외부 API 계약이 아니라 internal registry 구현 세부로 둔다.

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

- [ ] `apps/control-plane/internal/wire/edge_registry.go`: state에 internal connection token을 저장하고 stale disconnect를 무시한다.
- [ ] `apps/control-plane/internal/wire/edge_server.go`: connection별 token을 생성해 registry connect/disconnect에 전달한다.
- [ ] `apps/control-plane/internal/wire/edge_server_test.go`: 같은 `edge_id`로 새 hello가 들어온 뒤 오래된 disconnect가 새 연결을 disconnected로 만들지 않는 테스트를 추가한다.

#### 테스트 작성

테스트 작성. `TestEdgeServerStaleDisconnectDoesNotClearReconnect` 또는 동등한 이름으로 같은 `edge_id`의 두 connection을 열고, 첫 connection close 이후 registry가 두 번째 connection의 connected state를 유지하는지 확인한다.

#### 중간 검증

```bash
go test -race -count=1 ./apps/control-plane/internal/wire
```

기대: wire tests pass and race detector reports no race.

### [REVIEW_EDGE_CONN-2] Container and operator surface

#### 문제

`run()`은 `server.edge_wire_listen` 기본값 `0.0.0.0:19081`을 시작하지만 `apps/control-plane/Dockerfile`은 19081을 expose하지 않고 `docker-compose.yml`도 19081을 publish하지 않는다. compose/container 실행에서 외부 Edge가 새 endpoint에 연결할 수 없다. `apps/control-plane/README.md`도 현재 실행 표면에 `edge_wire_listen`을 설명하지 않는다.

#### 해결 방법

Control Plane Docker image와 compose dev service에 Edge wire port를 노출한다. compose에는 별도 host port env 이름을 사용해 Client WS port와 Edge TCP port가 섞이지 않게 한다. README의 현재 실행 표면과 local/dev override 설명도 새 config/env 이름과 일치시킨다.

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

- [ ] `apps/control-plane/Dockerfile`: `EXPOSE 9080 19080 19081`로 갱신한다.
- [ ] `docker-compose.yml`: `${IOP_CONTROL_PLANE_EDGE_WIRE_PORT:-19081}:19081` port mapping을 추가한다.
- [ ] `apps/control-plane/README.md`: `server.wire_listen`은 Client WS, `server.edge_wire_listen`은 Edge TCP로 분리해 설명하고 env override 이름을 반영한다.

#### 테스트 작성

신규 Go 테스트는 필요하지 않다. compose syntax와 정적 표면을 검증한다.

#### 중간 검증

```bash
docker compose config --quiet
rg --line-number "19081|IOP_CONTROL_PLANE_EDGE_WIRE_PORT|edge_wire_listen" apps/control-plane/Dockerfile docker-compose.yml apps/control-plane/README.md configs/control-plane.yaml
```

기대: compose config exits 0, and every operator-facing file shows the Edge TCP endpoint.

### [REVIEW_EDGE_CONN-3] Proto scope and evidence trust

#### 문제

현재 archived review evidence는 "proto 변경 없음"이라고 기록하지만 실제 diff는 `proto/iop/control.proto`, `proto/gen/iop/control.pb.go`, `apps/client/lib/gen/proto/iop/control.pb.dart`, `apps/client/lib/gen/proto/iop/control.pbjson.dart`를 변경한다. follow-up review evidence가 실제 변경 범위와 생성물 검증을 정확히 기록해야 한다.

#### 해결 방법

소스 변경이 추가로 필요 없다면 proto 파일은 그대로 두되, `CODE_REVIEW-*-G??.md`의 `계획 대비 변경 사항`, `주요 설계 결정`, `검증 결과`에 proto 원본과 Go/Dart 생성물 변경을 정확히 기록한다. 생성물은 workspace를 직접 덮어쓰지 않는 임시 디렉터리 방식으로 원본 proto와 일치하는지 비교한다.

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

- [ ] `CODE_REVIEW-cloud-G08.md`: proto 원본/생성물 변경과 검증 결과를 실제 diff와 일치하게 기록한다.

#### 테스트 작성

신규 테스트는 필요하지 않다. 생성물 일치 검증을 수행한다.

#### 중간 검증

```bash
tmpdir=$(mktemp -d)
protoc --go_out="$tmpdir" --go_opt=module=iop --proto_path=. proto/iop/runtime.proto proto/iop/node.proto proto/iop/control.proto proto/iop/job.proto
cmp -s proto/gen/iop/control.pb.go "$tmpdir/proto/gen/iop/control.pb.go"
rm -rf "$tmpdir"

tmpdir=$(mktemp -d)
mkdir -p "$tmpdir/apps/client/lib/gen"
protoc --plugin=protoc-gen-dart=/config/.local/bin/protoc-gen-dart --dart_out="$tmpdir/apps/client/lib/gen" --proto_path=. --proto_path=/config/.local/include proto/iop/runtime.proto proto/iop/node.proto proto/iop/control.proto proto/iop/job.proto
cmp -s apps/client/lib/gen/proto/iop/control.pb.dart "$tmpdir/apps/client/lib/gen/proto/iop/control.pb.dart"
cmp -s apps/client/lib/gen/proto/iop/control.pbjson.dart "$tmpdir/apps/client/lib/gen/proto/iop/control.pbjson.dart"
rm -rf "$tmpdir"
```

기대: all `cmp` commands exit 0.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/control-plane/internal/wire/edge_registry.go` | REVIEW_EDGE_CONN-1 |
| `apps/control-plane/internal/wire/edge_server.go` | REVIEW_EDGE_CONN-1 |
| `apps/control-plane/internal/wire/edge_server_test.go` | REVIEW_EDGE_CONN-1 |
| `apps/control-plane/Dockerfile` | REVIEW_EDGE_CONN-2 |
| `docker-compose.yml` | REVIEW_EDGE_CONN-2 |
| `apps/control-plane/README.md` | REVIEW_EDGE_CONN-2 |
| `CODE_REVIEW-cloud-G08.md` | REVIEW_EDGE_CONN-3 |

## 최종 검증

```bash
gofmt -l apps/control-plane/internal/wire/edge_registry.go apps/control-plane/internal/wire/edge_server.go apps/control-plane/internal/wire/edge_server_test.go
go test -count=1 ./apps/control-plane/...
go test -race -count=1 ./apps/control-plane/internal/wire
go test -count=1 ./packages/... ./proto/gen/...
```

기대: `gofmt -l` prints no files and all tests pass.

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