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

# Plan - ROUTE

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

구현 완료는 `CODE_REVIEW-local-G07.md` 작성 후 review-ready 보고까지다. review/finalization은 code-review 전용이다.

## 배경

Edge service와 OpenAI surface에는 provider-pool routing/status/refresh 일부가 있다. SDD S04/S05는 provider-first source of truth 기준으로 provider capacity, queue, health, model alias, served target을 처리하고 adapter duplicate snapshot을 만들지 않도록 요구한다.
dev-runtime에서 Mac `vllm-mlx` provider가 Metal OOM으로 종료된 뒤에도 정적 `health: healthy`와 `capacity: 3` 때문에 계속 dispatch 후보가 되어 `127.0.0.1:8002` connection refused가 반복됐다. `capacity: 0` workaround보다 운영 의도가 분명한 `nodes[].providers[].enabled: false` 스위치가 필요하다.

## 사용자 리뷰 요청 흐름

Milestone lock 결정만 review stub에 기록한다. field/runtime blocker는 검증 결과에 기록하고 follow-up으로 넘긴다.

## 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

## 분석 결과

### 읽은 파일

- `agent-roadmap/sdd/operational-observability-provider-management/node-provider-first-config-surface/SDD.md`
- `agent-contract/inner/edge-config-runtime-refresh.md`
- `agent-contract/inner/edge-node-runtime-wire.md`
- `agent-contract/outer/openai-compatible-api.md`
- `apps/edge/internal/service/service.go`
- `apps/edge/internal/service/run_dispatch.go`
- `apps/edge/internal/service/model_queue.go`
- `apps/edge/internal/service/status_provider.go`
- `apps/edge/internal/openai/routes.go`
- `apps/edge/internal/openai/server.go`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/responses_handler.go`
- `apps/edge/internal/configrefresh/classify.go`
- `apps/edge/internal/bootstrap/runtime.go`
- `apps/edge/internal/edgevalidate/validate.go`
- `apps/edge/internal/node/mapper.go`
- `packages/go/config/config.go`
- `packages/go/config/config_test.go`
- `apps/edge/internal/service/model_queue_test.go`
- `apps/edge/internal/configrefresh/classify_test.go`
- `apps/edge/internal/edgevalidate/validate_test.go`
- `apps/edge/internal/node/mapper_test.go`
- `configs/edge.yaml`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/node-smoke.md`
- `agent-test/local/platform-common-smoke.md`

### SDD 기준

대상 Acceptance Scenarios는 S04와 S05다. Evidence Map은 Edge service/OpenAI route tests, status/config refresh tests를 요구한다. provider-first compile 결과가 들어온 뒤 dispatch는 provider id와 served target을 기준으로 동작해야 한다. `providers[].enabled=false`는 S04/S05의 zero/disabled provider edge case로 취급하며, provider-pool 후보 제외와 status snapshot 표시가 같은 source of truth를 봐야 한다.

### 테스트 환경 규칙

`test_env=local`. `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/platform-common-smoke.md`를 적용한다. 공통 config schema와 edge service/config refresh를 함께 바꾸므로 적용 명령은 `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`이다. OpenAI-compatible 경계와 config 계약을 바꾸므로 최종 검증에 `go test ./apps/edge/... ./packages/go/config`와 `go test ./...` 보조 회귀를 둔다.

### 테스트 커버리지 공백

현재 `resolveProviderPoolCandidates`는 `providerDispatchable`에서 `prov.Adapter`를 요구한다(`apps/edge/internal/service/run_dispatch.go:474`). provider-first compile 이후에는 provider id/type로 adapter key를 도출해야 한다. `NodeProviderConf`에는 provider-level `enabled`가 없어 운영자가 provider를 명시적으로 내릴 방법이 `capacity: 0`뿐이다. status snapshot은 disabled provider의 effective capacity/status를 검증하지 않고, config refresh classification도 provider `enabled` live apply 분류를 모른다.

### 심볼 참조

rename/remove 없음. 새 helper 후보는 `ProviderEnabled` 또는 동등한 provider enabled resolver이며, 참조 대상은 config validation, edgevalidate provider reference check, dispatch candidate resolution, status/model queue snapshot, config refresh classification tests다.

### 분할 판단

`04+02,03_routing_status_refresh`는 compile과 compat가 완료된 뒤 routing/status/refresh를 하나의 edge ownership task로 묶는다. `providers[].enabled`는 새 public endpoint나 lifecycle manager가 아니라 provider-pool routing/status/refresh edge case라 이 subtask에 포함한다. 별도 split은 같은 disabled semantics를 config, dispatch, status, refresh에 중복 조율하게 만들어 더 위험하다.

### 범위 결정 근거

새 public endpoint 추가와 Control Plane canonical store 변경은 제외한다. `providers[].enabled=false`는 provider-pool dispatch/status 스위치이며 Node adapter process lifecycle을 시작/중지하거나 provider health probe를 자동화하지 않는다. full-cycle remote evidence는 `06+04,05_full_cycle_smoke`에서 실행한다.

### 빌드 등급

`local-G07`: Edge service, OpenAI surface, refresh runtime을 함께 건드리며 cross-package regression risk가 있다.

## 의존 관계 및 구현 순서

이 subtask는 디렉터리명 `04+02,03_routing_status_refresh`에 따라 같은 task group의 `02+01_normalize_compile`과 `03+01_legacy_compat`가 각각 `complete.log`를 만든 뒤 구현한다.

## 구현 체크리스트

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

### [ROUTE-1] Provider-First Routing Status Refresh

#### 문제

`apps/edge/internal/service/run_dispatch.go:477`는 provider dispatchability를 `prov.Adapter` 존재로 판정한다. provider-first schema에서는 provider id/type fields가 source of truth가 되고 `adapter`는 compat override가 된다. `apps/edge/internal/configrefresh/classify.go:81`의 provider diff key도 새 provider execution fields를 모르면 refresh 분류가 누락된다.
운영자가 provider를 의도적으로 내릴 때는 현재 `capacity: 0`을 써야 한다. 이 값은 capacity와 on/off 의도가 섞이고, status에서 실제 disabled 상태를 읽기 어렵다.

#### 해결 방법

provider runtime descriptor helper를 service/configrefresh 양쪽에서 일관되게 사용한다. dispatch는 provider id를 default adapter key로 사용하고 served target은 catalog `models[].providers` 값을 사용한다. refresh는 capacity/max_queue/queue_timeout은 live apply, provider type/category/execution fields/models/health/lifecycle 변화는 restart-required 또는 SDD가 허용한 applied class로 분류한다.
`NodeProviderConf`에는 생략 시 enabled로 해석되는 `enabled` 필드를 추가한다. Go zero-value `false`가 생략과 충돌하지 않도록 `*bool` 또는 동등한 tri-state 표현과 `ProviderEnabled(p)` helper를 사용한다. `enabled=false`는 provider-pool dispatch 후보에서 제외하고 status snapshot에는 `status=disabled`, `health=disabled`, effective `capacity=0`으로 표시한다. 이 스위치는 adapter lifecycle stop/remove가 아니므로 provider-derived adapter payload 제거까지 확대하지 않는다.

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

- [ ] `apps/edge/internal/service/run_dispatch.go`: provider-first key resolution and availability.
- [ ] `apps/edge/internal/service/model_queue.go`: snapshot adapter/id/load ratio behavior 보강.
- [ ] `apps/edge/internal/service/status_provider.go`: static snapshot parity 확인.
- [ ] `packages/go/config/config.go`: `NodeProviderConf.enabled` tri-state field and helper.
- [ ] `apps/edge/internal/edgevalidate/validate.go`: disabled provider validation/reference behavior.
- [ ] `apps/edge/internal/openai/*.go`: provider catalog route tests가 responses/chat both paths를 통과.
- [ ] `apps/edge/internal/configrefresh/classify.go`: provider-first fields diff 반영.
- [ ] `apps/edge/internal/bootstrap/runtime.go`: apply path model catalog/store/input manager refresh 유지 확인.
- [ ] `configs/edge.yaml`: provider-first example에 `enabled` 의미를 문서화.
- [ ] `agent-contract/inner/edge-config-runtime-refresh.md`: `providers[].enabled` default/live apply/status semantics 반영.
- [ ] 관련 `*_test.go` 추가/갱신.

#### 테스트 작성

작성한다. `TestLoadEdge_NodeProviderEnabledDefaultsTrue`, `TestResolveProviderPoolCandidatesSkipsDisabledProvider`, `TestProviderSnapshotsReportDisabledProvider`, `TestClassifyProviderEnabledLiveApply`, `TestResolveProviderPoolCandidatesUsesProviderIDWhenAdapterOmitted`, `TestStatusProviderProviderFirstNoAdapterDuplicates`, `TestClassifyProviderFirstExecutionFieldRestartRequired`, `TestResponsesProviderPoolDispatch`를 추가/갱신한다.

#### 중간 검증

```bash
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
```

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/service/run_dispatch.go` | ROUTE-1 |
| `apps/edge/internal/service/model_queue.go` | ROUTE-1 |
| `apps/edge/internal/service/status_provider.go` | ROUTE-1 |
| `packages/go/config/config.go` | ROUTE-1 |
| `packages/go/config/config_test.go` | ROUTE-1 |
| `apps/edge/internal/edgevalidate/validate.go` | ROUTE-1 |
| `apps/edge/internal/edgevalidate/validate_test.go` | ROUTE-1 |
| `apps/edge/internal/openai/*.go` | ROUTE-1 |
| `apps/edge/internal/configrefresh/classify.go` | ROUTE-1 |
| `apps/edge/internal/configrefresh/classify_test.go` | ROUTE-1 |
| `apps/edge/internal/bootstrap/runtime.go` | ROUTE-1 |
| `configs/edge.yaml` | ROUTE-1 |
| `agent-contract/inner/edge-config-runtime-refresh.md` | ROUTE-1 |
| 관련 `*_test.go` | ROUTE-1 |

## 최종 검증

```bash
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
```

```bash
go test ./apps/edge/... ./packages/go/config
```

```bash
go test ./...
```

Go test cache output is acceptable for broad edge regression; targeted package command should be run with the exact package list above.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
