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

# Plan - REVIEW_REVIEW_REFRESH_CONTRACT

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

이 계획은 이전 `REVIEW_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

- Current archived plan: `agent-task/m-runtime-reconnect-config-refresh/02_refresh_contract/plan_cloud_G07_1.log`
- Current archived review: `agent-task/m-runtime-reconnect-config-refresh/02_refresh_contract/code_review_cloud_G07_1.log`
- Verdict: FAIL
- Required summary:
  - `iop-edge serve` normalizes runtime config after load: `logging.path` is resolved with `edgecmd.ResolveEdgeLogPath`, and `bootstrap.artifact_dir` is resolved with `edgecmd.ResolveBootstrapArtifactDir`.
  - `configrefresh.LoadCandidate` loads and validates the candidate but does not apply the same runtime normalization.
  - `configrefresh.Classify` compares `bootstrap.artifact_dir` and full `logging` as `restart_required`, so source-level identical YAML can be classified as restart-required against a running serve-normalized process.
  - Existing running-runtime tests build `currentCfg` through `config.LoadEdge` directly, so they miss the real `serve` path mismatch.
- Prior loop context:
  - Earlier FAIL archive: `agent-task/m-runtime-reconnect-config-refresh/02_refresh_contract/plan_cloud_G07_0.log`, `agent-task/m-runtime-reconnect-config-refresh/02_refresh_contract/code_review_cloud_G07_0.log`.
  - The current loop fixed shared validation, thread-safe snapshots, model/provider diff classification, refresh templates, and final verification evidence.
- Affected files:
  - `apps/edge/cmd/edge/main.go`
  - `apps/edge/internal/edgecmd/paths.go`
  - `apps/edge/internal/configrefresh/classify.go`
  - `apps/edge/internal/configrefresh/classify_test.go`
  - `apps/edge/internal/bootstrap/runtime_test.go`
- Verification evidence from current review:
  - `go test -race -count=1 ./apps/edge/internal/bootstrap ./apps/edge/internal/service` passed.
  - `go test -count=1 ./apps/edge/... ./packages/go/... ./proto/gen/...` passed.
  - These commands are not sufficient to close the issue because no test covers a serve-normalized current config against a source-level refresh candidate.
- Allowed narrow archive reread:
  - Read only the four archive files listed above if exact prior-loop context is needed.

## 구현 체크리스트

- [ ] [REVIEW_REVIEW_REFRESH_CONTRACT-1] Refresh classification uses the same runtime-normalized config contract as `iop-edge serve` for `logging.path` and `bootstrap.artifact_dir`.
- [ ] [REVIEW_REVIEW_REFRESH_CONTRACT-2] Regression tests cover a serve-normalized current config plus source-level candidate YAML where only provider capacity changes and the result remains `applied`.
- [ ] [REVIEW_REVIEW_REFRESH_CONTRACT-3] Final verification commands are executed and the review stub records actual stdout/stderr.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## REVIEW_REVIEW_REFRESH_CONTRACT-1 - Runtime-Normalized Refresh Candidate

### 문제

`apps/edge/cmd/edge/main.go` loads `edge.yaml` and then mutates the runtime config into effective values:

```go
cfg.Logging.Path = logPath
cfg.Bootstrap.ArtifactDir = edgecmd.ResolveBootstrapArtifactDir(cfg.Bootstrap.ArtifactDir)
```

Refresh candidates are loaded through `configrefresh.LoadCandidate`, which currently calls only `config.LoadEdge` and shared validation. The classifier then compares `bootstrap.artifact_dir` and full `logging` as restart-required fields. In a real process started with `iop-edge serve`, a candidate file that is source-level identical except for provider capacity can be rejected as `restart_required` because the current runtime holds normalized path values and the candidate does not.

### 해결 방법

Introduce one shared runtime normalization boundary and use it consistently for serve and refresh. Prefer a small helper in an import-safe internal package that:

- resolves `logging.path` with the existing `edgecmd.ResolveEdgeLogPath` behavior,
- resolves `bootstrap.artifact_dir` with the existing `edgecmd.ResolveBootstrapArtifactDir` behavior,
- is called by `loadRuntimeConfig` and by refresh candidate loading before classification/apply.

If moving the helper would create an import cycle, keep the public behavior equivalent and document why the chosen package boundary is safe. Do not weaken the classifier by ignoring `logging` or `bootstrap.artifact_dir`; they should still be restart-required when their effective values actually change.

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

- [ ] Add or expose an import-safe helper for Edge runtime config normalization.
- [ ] `iop-edge serve` continues to use the same effective config normalization.
- [ ] `configrefresh.LoadCandidate` or the classification path normalizes candidates before diffing.
- [ ] Capacity-only candidate refresh no longer reports `logging` or `bootstrap.artifact_dir` as restart-required when the source config did not change.

### 중간 검증

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

## REVIEW_REVIEW_REFRESH_CONTRACT-2 - Serve-Path Regression Coverage

### 문제

Current tests load the running config with `config.LoadEdge` directly, which bypasses the `loadRuntimeConfig` normalization used by `serve`. This lets the bug hide behind matched unnormalized test fixtures.

### 해결 방법

Add focused regression coverage for the real mismatch:

- Build or simulate a current runtime config with serve-equivalent normalized values for `logging.path` and `bootstrap.artifact_dir`.
- Load a candidate YAML that uses source-level defaults or relative paths and changes only provider capacity.
- Assert dry-run classification is `applied`, includes the capacity change, and does not include `logging` or `bootstrap.artifact_dir` restart-required changes.
- When practical, also assert apply followed by a second dry-run produces no diff for the same candidate.

Keep the test deterministic; do not depend on a long-running external process or host-specific absolute paths beyond values produced by the shared helper.

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

- [ ] Add a classifier or runtime refresh test that fails on the archived bug.
- [ ] The test covers omitted `logging.path` or relative `bootstrap.artifact_dir` using the same source-level candidate YAML style operators will use.
- [ ] Existing S07/S08/S09 tests still pass and keep their intended statuses.

### 중간 검증

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

## 최종 검증

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

```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/...
```

예상: serve-normalized refresh regression, targeted race check, full Edge/config/proto tests가 모두 PASS.
