<!-- task=m-runtime-reconnect-config-refresh/04+02_config_push_contract plan=1 tag=REVIEW_NCPUSH -->

# Plan - REVIEW_NCPUSH

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성은 필수다. 구현 후 검증을 실행하고, 실제 구현 내용과 실제 stdout/stderr를 리뷰 stub에 채운 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 최종 archive, `complete.log`, roadmap 완료 처리는 code-review/runtime 책임이다. 선택된 Milestone의 `구현 잠금 > 결정 필요` 항목 때문에 구현이 막힐 때만 리뷰 stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 적고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하거나, `USER_REVIEW.md`/`complete.log`를 직접 만들지 않는다. 환경/secret/service 차단, 일반 범위 조정, 후속 에이전트가 닫을 수 있는 evidence 공백은 사용자 리뷰 요청이 아니다.

## 배경

이 follow-up은 `NCPUSH` 1차 구현 리뷰에서 발견된 Edge refresh push 계약 누락만 보완한다. Proto 메시지와 Node handler plumbing은 유지하되, Edge가 실제 refresh apply 시 configured node 전체에 대해 결과를 만들고, connected node에는 `request_id`와 node별 `NodeConfigPayload`를 채운 request를 보내도록 한다.

## 사용자 리뷰 요청 흐름

구현 중 사용자 리뷰 요청은 선택된 Milestone lock decision이 실제 구현을 막을 때만 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 해당 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 템플릿을 따른다. 구현 에이전트는 직접 사용자에게 질문하지 않으며, code-review가 요청 타당성을 검증하고 필요할 때만 `USER_REVIEW.md`를 작성한다.

## Archive Evidence Snapshot

- Current archived plan: `agent-task/m-runtime-reconnect-config-refresh/04+02_config_push_contract/plan_cloud_G07_0.log`
- Current archived review: `agent-task/m-runtime-reconnect-config-refresh/04+02_config_push_contract/code_review_cloud_G07_0.log`
- Verdict: FAIL
- Required summary:
  - `apps/edge/internal/transport/server.go:301` only iterates `registry.All()`, so configured-but-disconnected nodes are omitted instead of reported as `skipped`.
  - `apps/edge/internal/bootstrap/runtime.go:185`, `apps/edge/internal/bootstrap/runtime.go:222`, and `apps/edge/internal/bootstrap/runtime.go:226` do not propagate `configrefresh.Request.RequestID` or a node-specific `NodeConfigPayload` into `NodeConfigRefreshRequest`.
- Affected files: `apps/edge/internal/bootstrap/runtime.go`, `apps/edge/internal/bootstrap/runtime_test.go`, `apps/edge/internal/transport/server.go`, `apps/edge/internal/transport/server_test.go`, `apps/edge/internal/node/mapper.go`, `apps/edge/internal/node/store.go`.
- Verification evidence from failed loop: `make proto`, `go test -count=1 ./packages/go/... ./proto/gen/...`, `go test -count=1 ./apps/edge/...`, and `go test -count=1 ./apps/node/...` passed, but tests did not assert disconnected configured-node reporting, request id propagation, or payload contents.
- Narrow reread allowed: the two current archived log files listed above.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/update-plane-self-update-foundation/milestones/runtime-reconnect-config-refresh.md`
- Task ids:
  - `push-contract`: Edge-to-Node config refresh message 또는 command 계약이 proto/config 구조에 추가되고 backward compatibility 기준이 정리된다. 검증: generated proto와 handler 테스트가 통과한다.
- Completion mode: check-on-pass

## 범위 결정 근거

- 포함: Edge refresh push result enumeration, `NodeConfigRefreshRequest.RequestId` propagation, node별 `NodeConfigPayload` construction, Edge tests that prove skipped disconnected nodes and payload/request-id delivery.
- 제외: Node adapter registry live diff, in-flight drain, runtime concurrency/workspace live apply. Node가 payload를 받았을 때 `restart_required`로 응답하는 현재 경계는 유지한다.
- lane/GNN: `cloud-G07`. Public protobuf contract, Edge runtime, Edge transport, node payload mapping, SDD S12 evidence가 함께 걸려 있어 cloud review가 필요하다.

## 구현 체크리스트

- [ ] `Server.PushConfigRefresh`가 refreshed `NodeStore` record 전체를 기준으로 `node_results`를 만들고, registry에 없거나 live client가 없는 configured node를 `skipped`로 보고한다. 검증: Edge transport/runtime 테스트가 registry에 없는 configured node의 skipped result를 확인한다.
- [ ] `configrefresh.Request.RequestID`와 node별 `NodeConfigPayload`를 `NodeConfigRefreshRequest`에 채워 connected node로 보내고, fake node 테스트가 request id와 updated payload 내용을 검증한다. 검증: Edge runtime test가 fake node에서 `request_id`와 payload capacity/config를 assert한다.
- [ ] 전체 계약 회귀로 `make proto`, `go test -count=1 ./apps/edge/...`, `go test -count=1 ./apps/node/...`, `go test -count=1 ./packages/go/... ./proto/gen/...`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- 이 follow-up은 동일 split subtask `04+02_config_push_contract` 안의 FAIL 보완이다.
- 새 archive evidence는 현재 task 디렉터리의 `plan_cloud_G07_0.log`와 `code_review_cloud_G07_0.log`만 좁게 참조한다.
- PASS 전에는 후속 `05+04_node_adapter_diff`와 `06+05_node_drain_runtime_config`를 진행하지 않는다.

### [REVIEW_NCPUSH-1] Configured Disconnected Node Results

#### 문제

`PushConfigRefresh`가 `registry.All()`만 순회하면 Edge config에 존재하지만 현재 연결되지 않은 node는 결과에서 사라진다. 실제 disconnect listener는 registry entry를 제거하므로, 현재 테스트의 nil client entry 케이스는 실제 disconnected node 경로를 대표하지 못한다.

Before:

```go
entries := s.registry.All()
results := make([]configrefresh.NodeResult, 0, len(entries))
for _, entry := range entries {
    ...
}
```

#### 해결 방법

`NodeStore` snapshot의 `All()` record를 기준으로 결과를 만들고, 각 record ID로 registry live entry를 찾는다. live entry가 없거나 client가 nil/dead이면 `NodeResult{NodeID, Alias, Status:"skipped"}`를 append한다. connected entry에만 transport request를 보낸다. `PushConfigRefresh` 시그니처는 필요하면 `requestID`, `changedPaths`, `nodeStore`를 명시적으로 받도록 정리한다.

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

- [ ] `apps/edge/internal/transport/server.go`: configured node 전체를 기준으로 result를 생성한다.
- [ ] `apps/edge/internal/transport/server_test.go`: NodeStore에는 있지만 registry에는 없는 node가 `skipped`로 반환되는 테스트를 추가한다.
- [ ] `apps/edge/internal/bootstrap/runtime_test.go`: apply 결과가 connected node와 skipped node를 모두 포함하는지 확인한다.

#### 중간 검증

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

### [REVIEW_NCPUSH-2] Request ID And Payload Propagation

#### 문제

`RefreshConfig`가 받은 `RequestID`가 node refresh request로 전달되지 않고, `buildNodeConfigRefreshRequest`가 `ChangedPaths`만 채워 `Config`를 비워 둔다. 이 상태에서는 Node response가 어떤 refresh operation에 대한 ack인지 correlation할 수 없고, SDD S12가 요구하는 refresh payload push 계약도 실제 transport 경로에서 검증되지 않는다.

Before:

```go
nodeResults := r.Server.PushConfigRefresh(ctx, buildNodeConfigRefreshRequest(candidate, changedPaths))

func buildNodeConfigRefreshRequest(candidate *config.EdgeConfig, changedPaths []string) *iop.NodeConfigRefreshRequest {
    return &iop.NodeConfigRefreshRequest{ChangedPaths: changedPaths}
}
```

#### 해결 방법

`configrefresh.Request.RequestID`를 `applyMutableConfig`와 request builder까지 전달해 `NodeConfigRefreshRequest.RequestId`에 설정한다. connected node별로 refreshed `NodeStore` record를 찾아 `edgenode.BuildConfigPayload(record)` 결과를 `Config`에 채운다. Node에는 아직 live apply manager가 없으므로 non-empty payload/change에 대해 `restart_required`로 응답하는 현 Node behavior는 유지한다.

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

- [ ] `apps/edge/internal/bootstrap/runtime.go`: `req.RequestID`를 push request 생성 경로로 전달한다.
- [ ] `apps/edge/internal/transport/server.go`: connected node별 `NodeConfigPayload`를 request에 포함한다.
- [ ] `apps/edge/internal/bootstrap/runtime_test.go`: fake node listener가 `req.GetRequestId()=="node-result-test"`와 candidate payload의 updated config를 assert한다.
- [ ] `apps/edge/internal/transport/server_test.go`: request id와 payload가 transport request에 들어가는 단위 테스트를 추가하거나 runtime integration test로 충분히 검증한다.

#### 중간 검증

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

## 최종 검증

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

기대 결과: proto generation 성공, Edge/Node/platform-common package tests PASS. Go test cache output은 허용하지 않는다.
