<!-- task=m-runtime-reconnect-config-refresh/02_refresh_contract plan=1 tag=REVIEW_REFRESH_CONTRACT -->

# Plan - REVIEW_REFRESH_CONTRACT

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

이 계획은 이전 `REFRESH_CONTRACT` 구현의 코드리뷰 FAIL 항목만 해결한다. 구현 뒤에는 반드시 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용, 검증 출력, 계획 대비 변경 사항으로 채운다. review finalization, 로그 rename, `complete.log` 작성, archive 이동은 code-review 스킬 전용이다.

선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 review stub의 `사용자 리뷰 요청` 섹션에 정확한 연결 근거를 남기고 멈춘다. 직접 사용자에게 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이번 FAIL은 repo 내부 구현/검증으로 해소 가능한 항목이므로 기본적으로 사용자 리뷰 요청 대상이 아니다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/update-plane-self-update-foundation/milestones/runtime-reconnect-config-refresh.md`
- Task ids:
  - `entrypoint`: refresh MVP entrypoint가 정리되고 구현된다. CLI command/API/Control Plane command 중 선택한 표면은 실행 중 Edge process에 도달해야 하며 dry-run과 apply 결과를 구분한다.
  - `diff-classify`: config refresh는 기존 runtime config와 새 config의 diff를 산출하고 `applied`, `restart_required`, `rejected`로 변경을 분류한다.
- Completion mode: check-on-pass

## Archive Evidence Snapshot

- Archived plan: `agent-task/m-runtime-reconnect-config-refresh/02_refresh_contract/plan_cloud_G07_0.log`
- Archived review: `agent-task/m-runtime-reconnect-config-refresh/02_refresh_contract/code_review_cloud_G07_0.log`
- Verdict: FAIL
- Required summary:
  - Refresh candidate loading uses only `config.LoadEdge` while `config check` applies `validateEdgeConfig`, so invalid adapter/provider configs can be accepted by refresh.
  - Refresh apply mutates runtime config/model catalog without a thread-safe runtime/service snapshot boundary.
  - Classifier reports only provider capacity/queue fields and misses planned model catalog/provider structural diffs.
  - New `refresh` config contract is absent from `configs/edge.yaml` and hostsetup Edge templates.
- Affected files:
  - `apps/edge/internal/configrefresh/*.go`
  - `apps/edge/internal/bootstrap/runtime.go`
  - `apps/edge/internal/bootstrap/refresh_admin.go`
  - `apps/edge/internal/service/*.go`
  - `apps/edge/internal/edgecmd/config.go`
  - `apps/edge/internal/edgecmd/edgecmd_test.go`
  - `apps/edge/internal/bootstrap/runtime_test.go`
  - `packages/go/config/config.go`
  - `packages/go/config/config_test.go`
  - `packages/go/hostsetup/templates.go`
  - `packages/go/hostsetup/setup_test.go`
  - `configs/edge.yaml`
- Verification evidence from prior loop:
  - `go test -count=1 ./apps/edge/... ./packages/go/... ./proto/gen/...` passed in the archived review.
  - `go test -count=1 ./apps/edge/internal/edgecmd ./apps/edge/internal/bootstrap ./apps/edge/cmd/edge ./packages/go/config` passed in the archived review.
- Allowed narrow archive reread:
  - Read only the two archived files listed above if exact prior context is needed.

## 구현 체크리스트

- [ ] [REVIEW_REFRESH_CONTRACT-1] Refresh candidate validation is shared with `config check`, and invalid adapter/provider configs are rejected through `/refresh`.
- [ ] [REVIEW_REFRESH_CONTRACT-2] Refresh apply uses a thread-safe runtime/service snapshot boundary with deterministic concurrency coverage.
- [ ] [REVIEW_REFRESH_CONTRACT-3] Config diff classification reports model catalog/provider structural changes instead of silently returning `no changes detected`.
- [ ] [REVIEW_REFRESH_CONTRACT-4] The new `refresh` config contract is represented in user-facing Edge config examples/templates and covered by tests.
- [ ] [REVIEW_REFRESH_CONTRACT-5] Final verification commands are executed and the review stub records actual stdout/stderr.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## REVIEW_REFRESH_CONTRACT-1 - Shared Refresh Validation

### 문제

`configrefresh.LoadCandidate` only calls `config.LoadEdge`, while `iop-edge config check` additionally calls `validateEdgeConfig`. This violates the plan's same-validator requirement and S09 invalid provider config evidence.

### 해결 방법

Move the Edge runtime validation currently in `apps/edge/internal/edgecmd/config.go` into a non-CLI internal package or helper that both `config check` and `configrefresh.LoadCandidate` can call without an import cycle. Keep CLI command behavior unchanged.

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

- [ ] Shared validator is introduced outside the CLI command path.
- [ ] `config check` uses the shared validator.
- [ ] refresh candidate loading uses the shared validator.
- [ ] Add a refresh rejection test for at least one config that `validateEdgeConfig` currently rejects, such as enabled `vllm` or `openai_compat` with an empty endpoint.

### 중간 검증

```bash
go test -count=1 ./apps/edge/internal/configrefresh ./apps/edge/internal/edgecmd ./packages/go/config
```

## REVIEW_REFRESH_CONTRACT-2 - Thread-Safe Runtime Snapshot

### 문제

Refresh apply mutates `Runtime.Cfg` and `Service.modelCatalog` while request dispatch can read provider and catalog data. The current locks do not protect the shared backing arrays or service catalog reads.

### 해결 방법

Use a single safe runtime state boundary. Either make this contract phase classify/report only and defer mutation to the dependent apply plan, or implement copy-on-write replacement plus service/node-store locking so dispatch sees one consistent immutable snapshot. Do not mutate provider slices that are shared with `NodeStore` without the same synchronization used by dispatch/status readers.

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

- [ ] Runtime refresh reads a stable current snapshot.
- [ ] Apply does not race with provider pool dispatch, status snapshots, or model catalog reads.
- [ ] Tests cover apply during concurrent service reads or otherwise prove mutation is not performed in this phase.
- [ ] Targeted race verification is recorded.

### 중간 검증

```bash
go test -race -count=1 ./apps/edge/internal/bootstrap ./apps/edge/internal/service
```

## REVIEW_REFRESH_CONTRACT-3 - Complete Diff Classification

### 문제

The classifier only reports capacity/max_queue/queue_timeout changes. Planned mutable model catalog changes and unsupported provider structural changes can be hidden as `applied` with an empty `changes` list.

### 해결 방법

Extend `Classify` to compare the planned diff surface:

- `models[].providers` and model catalog entry changes should produce `applied` changes when valid and mutable.
- provider add/remove, adapter/type/category/models/health changes, and any unsupported node/provider structure changes should be classified explicitly as `restart_required` or `rejected`, depending on the chosen contract for this phase.
- mixed mutable and immutable changes must not be silently partial-applied.

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

- [ ] Model catalog/provider mapping changes are present in `Result.Changes`.
- [ ] Provider add/remove or unsupported provider field changes are not reported as `no changes detected`.
- [ ] Tests assert the exact status and at least one concrete changed path for these cases.

### 중간 검증

```bash
go test -count=1 ./apps/edge/internal/configrefresh
```

## REVIEW_REFRESH_CONTRACT-4 - Config Examples And Templates

### 문제

`packages/go/config.EdgeConfig` now has a `refresh` section, but neither the tracked `configs/edge.yaml` example nor hostsetup Edge templates show it. Operators cannot discover how to enable the local admin endpoint required by `iop-edge config refresh`.

### 해결 방법

Add a conservative default `refresh` block to `configs/edge.yaml`, the setup-installed Edge template, and the bundle-local `config init` template. Keep `enabled: false` by default and document the loopback listen address. Update hostsetup template tests.

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

- [ ] `configs/edge.yaml` includes `refresh.enabled` and `refresh.listen`.
- [ ] `packages/go/hostsetup/templates.go` Edge templates include the same default refresh block.
- [ ] hostsetup/config tests cover the template/default consistency.

### 중간 검증

```bash
go test -count=1 ./packages/go/hostsetup ./packages/go/config
```

## 최종 검증

```bash
go test -race -count=1 ./apps/edge/internal/bootstrap ./apps/edge/internal/service
```

```bash
go test -count=1 ./apps/edge/... ./packages/go/... ./proto/gen/...
```

예상: targeted race check와 Edge/config/proto generated package 테스트가 모두 PASS.
