<!-- task=04_edge_operation_contract plan=0 tag=OPC -->

# Code Review Reference - OPC

> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**

## 개요

date=2026-05-17
task=04_edge_operation_contract, plan=0, tag=OPC

## 이 파일을 읽는 리뷰 에이전트에게

구현과 검증 출력을 대조하고 PASS 시 plan/review를 log로 아카이브하세요.

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|---|---|
| [OPC-1] surface-neutral DTO 추가 | [x] |
| [OPC-2] opsconsole이 DTO를 우선 사용 | [x] |

## 계획 대비 변경 사항

- 계획에 있던 `RunDispatch`, `RunStream`, `NodeSnapshot`, `UsageStatusView`, `CommandResult`를 모두 `apps/edge/internal/service/service.go`에 추가했다.
- `RunHandle`은 새 DTO(`RunDispatch`, `RunStream`)를 embed하는 형태로 재구성해 기존 필드 접근(`handle.RunID`, `handle.Events` 등)을 보존했다.
- 기존 타입 이름은 alias로 유지: `type TerminateSessionResult = CommandResult`, `type UsageStatusResult = UsageStatusView`.
- `Service`에 `ListNodeSnapshots`, `ResolveNodeSnapshot`을 추가했다. 기존 `ListNodes`, `ResolveNode`는 그대로 두어 트랜스포트/registry 직접 접근이 필요한 경로(node lifecycle 등)와 호환을 유지한다.
- opsconsole `PrintNodes` 시그니처를 `[]*edgenode.NodeEntry` → `[]edgeservice.NodeSnapshot`로 변경했다. 호출부는 단일이고, 결과 출력 포맷은 동일하다.
- opsconsole `SendStatus`는 `ResolveNode` + 인라인 label 계산을 `ResolveNodeSnapshot`으로 대체했다.
- `events.go`(run event 렌더링)는 그대로 두었다. 계획대로 proto event 렌더링은 event router 내부에 격리되어 있으며, run dispatch 경로만 DTO를 우선 사용한다.

## 주요 설계 결정

- **proto 노출 기준**: 비동기 event stream(`*iop.RunEvent`, `*iop.EdgeNodeEvent`)은 `RunStream`이 그대로 노출한다. event router로 격리되어 있고 HTTP/API도 동일한 wire schema를 따를 가능성이 높기 때문에 이번 단계에서 추상화하지 않는다. 반면 dispatch/terminate/status의 결과 metadata는 모두 surface-neutral DTO로 감쌌다.
- **alias 사용**: 기존 caller(특히 opsconsole 내부 비테스트 코드와 다른 잠재 caller)를 깨지 않기 위해 `TerminateSessionResult`, `UsageStatusResult`는 type alias로 유지했다. 새 코드는 `CommandResult`, `UsageStatusView`를 사용하면 된다.
- **embed 채택**: `RunHandle` 필드를 보존하기 위해 `RunDispatch`, `RunStream`을 embed 했다. 새로운 HTTP handler는 `*RunHandle` 대신 `RunDispatch`/`RunStream`을 직접 받을 수 있다.
- **NodeSnapshot.Label**: alias가 비어 있을 때 NodeID로 fallback 하는 label을 service에서 계산해 caller가 동일 로직을 중복 작성하지 않게 했다.

## 리뷰어를 위한 체크포인트

- HTTP/API 추가 전제의 DTO가 console 전용 이름을 갖지 않는가. → `NodeSnapshot`, `RunDispatch`, `RunStream`, `UsageStatusView`, `CommandResult` 모두 transport-agnostic 이름이다.
- 기존 service callers가 깨지지 않는가. → `RunHandle.*`, `UsageStatusResult`, `TerminateSessionResult` 접근부 보존. `ListNodes`/`ResolveNode` 미변경. 전체 빌드/테스트 통과.
- proto 타입 노출이 필요한 곳과 숨긴 곳의 기준이 명확한가. → 이벤트 stream만 proto, 그 외 응답 metadata는 DTO. `UsageStatusView.UsageStatus`만 proto payload를 그대로 들고 있다(렌더링이 proto getter에 의존).

## 검증 결과

### `go test -count=1 ./apps/edge/internal/service`

```
ok  	iop/apps/edge/internal/service	0.003s
```

### `go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/opsconsole ./apps/edge/cmd/edge`

```
ok  	iop/apps/edge/internal/service	0.003s
ok  	iop/apps/edge/internal/opsconsole	0.005s
?   	iop/apps/edge/cmd/edge	[no test files]
```

### `go test -count=1 ./...`

```
?   	iop/apps/control-plane/cmd/control-plane	[no test files]
?   	iop/apps/edge/cmd/edge	[no test files]
ok  	iop/apps/edge/internal/bootstrap	0.006s
ok  	iop/apps/edge/internal/events	0.003s
ok  	iop/apps/edge/internal/node	0.004s
ok  	iop/apps/edge/internal/opsconsole	0.008s
ok  	iop/apps/edge/internal/service	0.004s
ok  	iop/apps/edge/internal/transport	0.007s
?   	iop/apps/node/cmd/node	[no test files]
ok  	iop/apps/node/internal/adapters	0.005s
ok  	iop/apps/node/internal/adapters/cli	87.783s
?   	iop/apps/node/internal/adapters/cli/internal/testutil	[no test files]
ok  	iop/apps/node/internal/adapters/cli/status	31.034s
?   	iop/apps/node/internal/adapters/mock	[no test files]
?   	iop/apps/node/internal/adapters/ollama	[no test files]
?   	iop/apps/node/internal/adapters/vllm	[no test files]
ok  	iop/apps/node/internal/bootstrap	0.161s
ok  	iop/apps/node/internal/node	0.009s
ok  	iop/apps/node/internal/router	0.006s
?   	iop/apps/node/internal/runtime	[no test files]
ok  	iop/apps/node/internal/store	0.052s
ok  	iop/apps/node/internal/transport	0.007s
?   	iop/apps/worker/cmd/worker	[no test files]
?   	iop/packages/auth	[no test files]
ok  	iop/packages/config	0.008s
?   	iop/packages/events	[no test files]
?   	iop/packages/jobs	[no test files]
?   	iop/packages/metadata	[no test files]
?   	iop/packages/observability	[no test files]
?   	iop/packages/policy	[no test files]
?   	iop/packages/version	[no test files]
```

### 추가된/수정된 테스트

- `TestListNodesReturnsSnapshots` (`apps/edge/internal/service/service_test.go`)
- `TestResolveNodeSnapshotReturnsDTO` (`apps/edge/internal/service/service_test.go`)
- `TestSubmitRunReturnsDispatchMetadata` (`apps/edge/internal/service/service_test.go`)
- `TestPrintNodes_ShowsSelectedNode` — DTO 입력으로 변경 (`apps/edge/internal/opsconsole/console_test.go`)
- `TestPrintNodes_AcceptsSnapshotDTO` — DTO fallback test 추가 (`apps/edge/internal/opsconsole/console_test.go`)

---

## 코드리뷰 결과

### 종합 판정: FAIL

### 차원별 평가

| 차원 | 평가 | 근거 |
|---|---|---|
| Correctness | Pass | DTO 추가, alias 보존, opsconsole call-site 전환 자체는 대상 패키지 테스트와 전체 테스트에서 동작 회귀가 확인되지 않았다. |
| Completeness | Fail | edge service/opsconsole 변경은 사용자 실행 파이프라인에 닿지만, testing domain rule의 E2E smoke/full-cycle 검증이 기록되지 않았다. |
| Test coverage | Fail | unit/package/full Go 테스트는 재현 통과했으나 필수 사용자 실행 검증이 누락되었다. |
| API contract | Pass | `RunHandle` embedded field 접근, `TerminateSessionResult`/`UsageStatusResult` alias, `ListNodes`/`ResolveNode` 유지가 확인되었다. |
| Code quality | Fail | `apps/edge/internal/service/service_test.go`가 `gofmt` clean 상태가 아니다. |
| Plan deviation | Warn | 계획의 Go 테스트 범위는 수행됐지만 프로젝트 testing rule 기준 검증 범위가 부족하다. |
| Verification trust | Fail | 기록된 `go test` 출력은 재현됐지만, `gofmt -l` 실패와 필수 E2E/full-cycle 검증 누락 때문에 완료 검증으로 신뢰할 수 없다. |

### 발견된 문제

- Required: `apps/edge/internal/service/service_test.go:86` 및 `apps/edge/internal/service/service_test.go:104`의 composite literal field alignment가 `gofmt` 결과와 다르다. `gofmt -l apps/edge/internal/service/service_test.go`가 해당 파일을 출력하며, `gofmt -d`는 `Adapter`/`Target`/`Prompt`/`Metadata` alignment 변경만 제안한다. 수정: `gofmt -w apps/edge/internal/service/service_test.go`를 적용하고 `gofmt -l`이 빈 출력인지 확인한다.
- Required: `agent-task/04_edge_operation_contract/CODE_REVIEW-cloud-G08.md:48`의 검증 결과는 Go 테스트만 기록한다. 이번 변경은 `apps/edge/internal/service/service.go`와 `apps/edge/internal/opsconsole/console.go`의 edge service/console 경로에 닿으므로 `agent-ops/rules/project/domain/testing/rules.md` 기준 mock E2E smoke와 full-cycle 실제 구동 수행 여부가 보고되어야 한다. 수정: `agent-ops/skills/project/e2e-smoke/SKILL.md` 절차에 따라 임시 config/포트 기반 `bin/edge.sh` + `bin/node.sh` 검증을 수행하고, 수행 불가 항목은 profile/cycle별 blocker와 남은 위험을 `CODE_REVIEW-cloud-G08.md`에 기록한다.

### 리뷰어 재현 검증

```bash
$ gofmt -l apps/edge/internal/service/service.go apps/edge/internal/service/service_test.go apps/edge/internal/opsconsole/console.go apps/edge/internal/opsconsole/events.go apps/edge/internal/opsconsole/status.go apps/edge/internal/opsconsole/console_test.go apps/edge/internal/opsconsole/events_test.go apps/edge/internal/bootstrap/module.go apps/edge/internal/bootstrap/runtime.go apps/edge/internal/bootstrap/runtime_test.go apps/edge/internal/transport/server.go apps/edge/cmd/edge/console.go
apps/edge/internal/service/service_test.go
```

```bash
$ git diff --check -- apps/edge/internal/service/service.go apps/edge/internal/service/service_test.go apps/edge/internal/opsconsole apps/edge/internal/bootstrap apps/edge/internal/transport/server.go apps/edge/cmd/edge/console.go
# no output
```

```bash
$ go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/opsconsole ./apps/edge/cmd/edge
ok  	iop/apps/edge/internal/service	0.003s
ok  	iop/apps/edge/internal/opsconsole	0.004s
?   	iop/apps/edge/cmd/edge	[no test files]
```

```bash
$ go test -count=1 ./...
?   	iop/apps/control-plane/cmd/control-plane	[no test files]
?   	iop/apps/edge/cmd/edge	[no test files]
ok  	iop/apps/edge/internal/bootstrap	0.007s
ok  	iop/apps/edge/internal/events	0.004s
ok  	iop/apps/edge/internal/node	0.004s
ok  	iop/apps/edge/internal/opsconsole	0.006s
ok  	iop/apps/edge/internal/service	0.005s
ok  	iop/apps/edge/internal/transport	0.011s
?   	iop/apps/node/cmd/node	[no test files]
ok  	iop/apps/node/internal/adapters	0.006s
ok  	iop/apps/node/internal/adapters/cli	88.058s
?   	iop/apps/node/internal/adapters/cli/internal/testutil	[no test files]
ok  	iop/apps/node/internal/adapters/cli/status	31.188s
?   	iop/apps/node/internal/adapters/mock	[no test files]
?   	iop/apps/node/internal/adapters/ollama	[no test files]
?   	iop/apps/node/internal/adapters/vllm	[no test files]
ok  	iop/apps/node/internal/bootstrap	0.160s
ok  	iop/apps/node/internal/node	0.009s
ok  	iop/apps/node/internal/router	0.004s
?   	iop/apps/node/internal/runtime	[no test files]
ok  	iop/apps/node/internal/store	0.031s
ok  	iop/apps/node/internal/transport	0.006s
?   	iop/apps/worker/cmd/worker	[no test files]
?   	iop/packages/auth	[no test files]
ok  	iop/packages/config	0.008s
?   	iop/packages/events	[no test files]
?   	iop/packages/jobs	[no test files]
?   	iop/packages/metadata	[no test files]
?   	iop/packages/observability	[no test files]
?   	iop/packages/policy	[no test files]
?   	iop/packages/version	[no test files]
?   	iop/proto/gen/iop	[no test files]
```

### 다음 단계

FAIL: Required 이슈를 반영한 새 routed plan/review 파일을 작성해 후속 구현 루프를 계속한다.
