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

# Plan - REVIEW_REVIEW_REVIEW_REFRESH_CONTRACT

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

이 계획은 이전 `REVIEW_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_local_G05_2.log`
- Current archived review: `agent-task/m-runtime-reconnect-config-refresh/02_refresh_contract/code_review_local_G05_2.log`
- Verdict: FAIL
- Required summary:
  - `configrefresh.applyRuntimeNormalization` cleans explicitly configured relative `logging.path` values with `filepath.Clean`.
  - `iop-edge serve` does not clean explicit log paths; `edgecmd.ResolveEdgeLogPath` returns non-empty explicit values unchanged.
  - Therefore source-identical YAML with `logging.path: "./logs/edge.log"` can still be classified as `restart_required` against the running process when only provider capacity changed.
  - The new regression tests cover empty `logging.path`, but not explicit relative `logging.path`, and the test helper duplicates the same clean behavior.
- Prior loop context:
  - Earlier archives: `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`, `agent-task/m-runtime-reconnect-config-refresh/02_refresh_contract/plan_cloud_G07_1.log`, `agent-task/m-runtime-reconnect-config-refresh/02_refresh_contract/code_review_cloud_G07_1.log`.
  - Current loop verification commands all passed, but they do not cover explicit relative log paths.
- Affected files:
  - `apps/edge/internal/configrefresh/classify.go`
  - `apps/edge/internal/configrefresh/classify_test.go`
  - `apps/edge/internal/bootstrap/runtime_test.go` if runtime-level helper duplication is adjusted
  - `apps/edge/internal/edgecmd/paths.go` or a new import-safe helper location if normalization is shared
- Verification evidence from current review:
  - `go test -count=1 ./apps/edge/internal/configrefresh ./apps/edge/internal/bootstrap ./apps/edge/cmd/edge ./apps/edge/internal/edgecmd` passed.
  - `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.
- Allowed narrow archive reread:
  - Read only the six archive files listed above if exact prior-loop context is needed.

## 구현 체크리스트

- [x] [REVIEW_REVIEW_REVIEW_REFRESH_CONTRACT-1] Refresh candidate logging path normalization exactly matches `iop-edge serve` for empty, absolute, and explicit relative `logging.path`.
- [x] [REVIEW_REVIEW_REVIEW_REFRESH_CONTRACT-2] Regression tests cover explicit relative `logging.path` with capacity-only refresh and prove no `logging` restart-required diff is produced.
- [x] [REVIEW_REVIEW_REVIEW_REFRESH_CONTRACT-3] Final verification commands are executed and the review stub records actual stdout/stderr.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## REVIEW_REVIEW_REVIEW_REFRESH_CONTRACT-1 - Exact Serve Logging Path Contract

### 문제

`edgecmd.ResolveEdgeLogPath` keeps explicit values unchanged:

```go
if explicit != "" {
	return explicit, nil
}
```

`configrefresh.applyRuntimeNormalization` currently calls `filepath.Clean` for explicit relative paths. That changes source-level values such as `./logs/edge.log` to `logs/edge.log`, while a running `serve` process keeps `./logs/edge.log`. Since `Classify` treats `logging` as restart-required, a capacity-only refresh can be blocked by this false diff.

### 해결 방법

Make the refresh candidate path normalization exactly match the effective `serve` behavior:

- empty `logging.path`: resolve to `<binary-dir>/logs/edge.log`,
- absolute `logging.path`: preserve exactly,
- explicit relative `logging.path`: preserve exactly, including `./` segments.

Prefer moving the existing effective-config normalization into an import-safe helper used by both `apps/edge/cmd/edge/main.go` and `configrefresh.LoadCandidate`. If that is too broad for this follow-up, keep the implementation minimal but add comments/tests that lock the exact equivalence.

Do not remove `logging` from restart-required classification. Real effective logging changes must still produce `restart_required`.

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

- [x] Remove the `filepath.Clean` behavior for explicit relative `logging.path` in refresh normalization, or replace duplicated code with a shared helper that preserves explicit values like `serve`.
- [x] Keep existing empty-path normalization behavior aligned with `edgecmd.ResolveEdgeLogPath`.
- [x] Keep `bootstrap.artifact_dir` relative path behavior aligned with `edgecmd.ResolveBootstrapArtifactDir`.

### 중간 검증

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

## REVIEW_REVIEW_REVIEW_REFRESH_CONTRACT-2 - Explicit Relative Path Regression

### 문제

The current tests prove empty `logging.path` works but do not catch explicit relative path drift. Their test helper also cleans relative paths, mirroring the buggy refresh behavior rather than `serve`.

### 해결 방법

Add a focused regression test that fails before the fix:

- current config mimics `serve` with `logging.path: "./logs/edge.log"` preserved exactly,
- candidate YAML uses the same `logging.path: "./logs/edge.log"` and changes only provider capacity,
- dry-run result is `applied`,
- result changes contain the provider capacity diff,
- no `logging` restart-required diff appears.

If using a helper to build serve-normalized current config, make the helper preserve explicit relative log paths exactly.

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

- [x] Add or update a configrefresh test for explicit relative `logging.path`.
- [x] Update any test helper so it models `edgecmd.ResolveEdgeLogPath` exactly for explicit values.
- [x] Existing empty logging path regression remains covered.

### 중간 검증

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

예상: explicit relative logging path 회귀 테스트, targeted race check, full Edge/config/proto tests가 모두 PASS.
