<!-- task=m-node-provider-first-config-surface/04+02,03_routing_status_refresh plan=0 tag=ROUTE -->

# Code Review Reference - ROUTE

> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> Fill implementation-owned sections, then stop with active files in place and report ready for review.

## 개요

date=2026-06-30
task=m-node-provider-first-config-surface/04+02,03_routing_status_refresh, plan=0, tag=ROUTE

## Roadmap Targets

- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-provider-first-config-surface.md`
- Task ids:
  - `routing-status-refresh`: provider-first dispatch, status snapshot, config refresh
- Completion mode: check-on-pass

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [ROUTE-1] Provider-First Routing Status Refresh | [x] |

## 구현 체크리스트

- [x] provider-first source에서 adapter key/served target/capacity/queue/health를 resolve하도록 dispatch 후보 생성 로직을 갱신한다.
- [x] `nodes[].providers[].enabled`를 생략 시 enabled, `false` 시 provider-pool dispatch 제외로 해석하는 config schema/helper/tests를 추가한다.
- [x] status snapshot이 provider catalog를 우선하고 zero/disabled provider edge case를 `status=disabled`, effective capacity 0으로 명확히 표현하도록 유지/보강한다.
- [x] config refresh classify/apply가 provider-first mutable/restart fields와 `providers[].enabled` live apply를 SDD 기준으로 분류하고 runtime catalog/store를 갱신하도록 한다.
- [x] OpenAI `/v1/models`, `/v1/responses`, `/v1/chat/completions` route tests가 provider-first catalog dispatch를 검증하도록 추가/갱신한다.
- [x] `go test ./packages/go/config ./apps/edge/internal/edgevalidate ./apps/edge/internal/node ./apps/edge/internal/service ./apps/edge/internal/openai ./apps/edge/internal/configrefresh ./apps/edge/internal/bootstrap`를 실행한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 코드리뷰 전용 체크리스트

> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.

- [x] 판정을 append한다.
- [x] active plan/review를 `.log`로 아카이브한다.
- [ ] PASS이면 `complete.log` 작성 후 task directory를 archive로 이동한다.

## 계획 대비 변경 사항

- **`providerDispatchable()` → `providerAdapterKey()`**: 계획은 provider-first dispatch key resolve를 별도 helper로 나눴으나, 실구현에서 `providerDispatchable()`을 `providerAdapterKey()`로 rename하고 반환값을 `string`으로 변경했다. loop 안에서 `ProviderEnabled` 체크와 provider-first/legacy 분기를 같이 처리한다.
- **`p.Validate()` 활용**: `edgevalidate/validate.go`에서 disabled provider structural 검증에 `config.ValidateCategory()` 직접 호출 대신 `p.Validate()`를 사용했다. `ValidateCategory`가 unexported 함수라 호출 불가였으므로 `p.Validate()`로 대체했다. 동일 검증 내용을 커버한다.
- **classify_test.go 내 YAML 어댑터 필드 선택**: `TestClassifyProviderFirstExecutionFieldRestartRequired`에서 provider type 변경(`vllm` → `openai_compat`)을 restart-required 대상으로 사용했다. endpoint 변경은 현재 classify.go가 provider-level raw field diff를 직접 보지 않고 providerKey 구조 필드 diff만 보므로, provider type/category 변경이 key diff를 유발하는 경로로 구성했다.

## 주요 설계 결정

### `*bool` tri-state for `enabled`
Go zero-value `false`가 "생략 = enabled" 의미와 충돌한다. `*bool` (nil = enabled, `*false` = disabled)로 정의하고 `ProviderEnabled(p NodeProviderConf) bool` helper로 추상화했다. config load/test/dispatch/status/classify 모두 이 helper 하나를 통해 판정한다.

### provider-first dispatch key (`providerAdapterKey`)
`prov.Adapter != ""` → 기존 adapter instance key 사용 (legacy compat path).  
`prov.Adapter == ""` → `prov.ID`를 dispatch key로 사용 (provider-first path).  
Node는 provider ID를 수신해 내부 실행 config를 결정한다. `isProviderAdapterInstanceValid` 호출은 legacy path에서만 수행한다.

### disabled provider validation 분리
disabled provider는 adapter reference check를 skip하지만 `p.Validate()`(type/category/models/numeric bounds)는 수행한다. 이는 "disable해도 config 형식 오류는 잡아야 한다"는 운영 관점과 일치한다.

### classify.go `providerKey.Enabled` 추가
`buildProviderIndex`에서 `Enabled: config.ProviderEnabled(p)` 설정 후, candidateProviders loop 안에서 `cur.Enabled != cp.Enabled` diff를 감지해 `StatusApplied` change를 emit한다. `providerKey` struct 자체는 restart-required structural comparison에 사용되므로, `Enabled` 필드 변화는 별도 `if` 블록에서만 처리한다.

## 사용자 리뷰 요청

- 상태: 없음
- 사유 유형: 없음
- 연결 대상: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음

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

- provider-first route가 provider id/type에서 dispatch key를 얻는지 확인한다.
- `providers[].enabled=false`가 dispatch 후보에서 제외되고 status에 disabled/effective capacity 0으로 드러나는지 확인한다.
- status snapshot에 adapter duplicate가 생기지 않는지 확인한다.
- refresh classification이 `providers[].enabled` live apply와 restart-required를 SDD 기준으로 나누는지 확인한다.

## 검증 결과

### ROUTE-1 중간 검증

```text
$ go test ./packages/go/config ./apps/edge/internal/edgevalidate ./apps/edge/internal/node ./apps/edge/internal/service ./apps/edge/internal/openai ./apps/edge/internal/configrefresh ./apps/edge/internal/bootstrap
ok  	iop/packages/go/config	0.040s
ok  	iop/apps/edge/internal/edgevalidate	0.005s
ok  	iop/apps/edge/internal/node	0.004s
ok  	iop/apps/edge/internal/service	0.439s
ok  	iop/apps/edge/internal/openai	1.512s
ok  	iop/apps/edge/internal/configrefresh	0.011s
ok  	iop/apps/edge/internal/bootstrap	0.258s
```

### 최종 검증

```text
$ go test ./apps/edge/... ./packages/go/config
ok  	iop/apps/edge/cmd/edge	0.032s
ok  	iop/apps/edge/internal/bootstrap	(cached)
ok  	iop/apps/edge/internal/configrefresh	(cached)
ok  	iop/apps/edge/internal/controlplane	4.450s
ok  	iop/apps/edge/internal/edgecmd	0.013s
ok  	iop/apps/edge/internal/edgevalidate	(cached)
ok  	iop/apps/edge/internal/events	(cached)
ok  	iop/apps/edge/internal/input	0.008s
ok  	iop/apps/edge/internal/input/a2a	0.006s
ok  	iop/apps/edge/internal/node	(cached)
ok  	iop/apps/edge/internal/openai	(cached)
ok  	iop/apps/edge/internal/opsconsole	0.008s
ok  	iop/apps/edge/internal/service	(cached)
ok  	iop/apps/edge/internal/transport	2.040s
ok  	iop/packages/go/config	(cached)
```

```text
$ go test ./...
ok  	iop/apps/control-plane/cmd/control-plane	(cached)
ok  	iop/apps/control-plane/internal/wire	1.470s
ok  	iop/apps/edge/cmd/edge	(cached)
ok  	iop/apps/edge/internal/bootstrap	(cached)
ok  	iop/apps/edge/internal/configrefresh	(cached)
ok  	iop/apps/edge/internal/controlplane	(cached)
ok  	iop/apps/edge/internal/edgecmd	(cached)
ok  	iop/apps/edge/internal/edgevalidate	(cached)
ok  	iop/apps/edge/internal/events	(cached)
ok  	iop/apps/edge/internal/input	(cached)
ok  	iop/apps/edge/internal/input/a2a	(cached)
ok  	iop/apps/edge/internal/node	(cached)
ok  	iop/apps/edge/internal/openai	(cached)
ok  	iop/apps/edge/internal/opsconsole	(cached)
ok  	iop/apps/edge/internal/service	(cached)
ok  	iop/apps/edge/internal/transport	(cached)
ok  	iop/apps/node/cmd/node	0.009s
ok  	iop/apps/node/internal/adapters	0.009s
ok  	iop/apps/node/internal/adapters/cli	46.513s
ok  	iop/apps/node/internal/adapters/cli/status	(cached)
ok  	iop/apps/node/internal/adapters/ollama	(cached)
ok  	iop/apps/node/internal/adapters/openai_compat	(cached)
ok  	iop/apps/node/internal/adapters/vllm	(cached)
ok  	iop/apps/node/internal/bootstrap	0.440s
ok  	iop/apps/node/internal/node	0.290s
ok  	iop/apps/node/internal/router	(cached)
ok  	iop/apps/node/internal/store	(cached)
ok  	iop/apps/node/internal/terminal	(cached)
ok  	iop/apps/node/internal/transport	(cached)
ok  	iop/packages/go/audit	(cached)
ok  	iop/packages/go/config	(cached)
ok  	iop/packages/go/hostsetup	(cached)
ok  	iop/packages/go/observability	(cached)
```

## 코드리뷰 결과

- 종합 판정: FAIL
- 차원별 평가:
  - Correctness: Fail
  - Completeness: Fail
  - Test coverage: Fail
  - API contract: Fail
  - Code quality: Pass
  - Implementation deviation: Fail
  - Verification trust: Fail
  - Spec conformance: Fail
- 발견된 문제:
  - Required: `apps/edge/internal/configrefresh/classify.go:82`의 `providerKey`와 `apps/edge/internal/configrefresh/classify.go:254`-`260`의 restart-required 비교가 provider-first 실행 필드(`provider`, `endpoint`, `base_url`, `headers`, `command`, `args`, `env`, `mode`, `resume_args`, `output_format`, `context_size`, `request_timeout_ms`)를 전혀 포함하지 않습니다. 그래서 같은 provider id/type에서 endpoint나 command만 바뀌면 `Classify`가 no-change/applied로 판단할 수 있고, apply 경로는 `apps/edge/internal/bootstrap/runtime.go:220`-`224`에서 변경 목록이 비어 있으면 Node config refresh push까지 생략합니다. 이는 `agent-contract/inner/edge-config-runtime-refresh.md`의 provider execution field restart-required 계약과 SDD S05 Evidence Map을 깨며, Edge snapshot과 Node runtime config가 갈라질 수 있습니다. Fix: provider diff key에 provider-first 실행 필드를 추가하고, 해당 필드 변경을 `StatusRestartRequired`로 분류하세요. endpoint-only/base_url-only/command-or-headers-only 변경 테스트를 추가해 type 변경 없이도 restart_required가 나오는지 확인해야 합니다.
- 다음 단계: FAIL follow-up plan/review를 생성한다.
