<!-- task=m-provider-catalog-device-status/03_catalog_status_http plan=0 tag=API -->

# PLAN API - Control Plane Provider Status JSON

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

이 plan의 구현은 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성까지 끝나야 완료다. 코드 변경 후 검증 명령을 실행하고 실제 stdout/stderr를 review stub에 붙인 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review 스킬 전용이다.

구현 중 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 막으면 review stub의 `사용자 리뷰 요청` 섹션에 연결 대상, 차단 근거, 실행한 명령, 재개 조건을 기록하고 멈춘다. 직접 사용자에게 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경, secret, service 준비, 일반 범위 변경, 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과 또는 후속 plan 대상으로 기록한다.

## 배경

Edge wire/proto에는 `EdgeNodeSnapshot.provider_snapshots`와 `ProviderSnapshot`의 catalog/status 필드가 이미 있다. 그러나 Control Plane HTTP `/edges/{id}/status` 변환은 node의 config summary만 내보내고 provider snapshots를 버린다. 이 때문에 Edge-owned read-only provider status가 Control Plane과 Client 표면으로 전달되지 않아 `device-status`를 닫을 수 없다.

## 사용자 리뷰 요청 흐름

사용자 리뷰 요청은 선택된 SDD 결정 또는 선택된 Milestone lock decision만 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 중 직접 사용자 프롬프트는 금지하며, code-review가 정당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
- Task ids:
  - `device-status`: provider health, capacity, in-flight, derived load ratio, served models, queued 상태가 catalog/status 표면에 정리된다.
- Completion mode: check-on-pass

## Spec Targets

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/SDD.md`
- Acceptance scenarios:
  - `S07`: task=`device-status`; evidence=`Control Plane HTTP status JSON exposes provider health, capacity, in_flight, derived load_ratio, served_models, queued without runtime/resource telemetry.`
- Completion mode: spec-check-on-pass

## 분석 결과

### 읽은 파일

- `AGENTS.md`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/update-roadmap/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`
- `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
- `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/SDD.md`
- `agent-ops/rules/project/domain/control-plane/rules.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/control-plane-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `proto/iop/control.proto`
- `proto/gen/iop/control.pb.go`
- `proto/iop/runtime.proto`
- `proto/gen/iop/runtime.pb.go`
- `apps/control-plane/cmd/control-plane/http_views.go`
- `apps/control-plane/cmd/control-plane/main_test.go`
- `apps/edge/internal/controlplane/connector.go`
- `apps/edge/internal/service/status_provider.go`
- `apps/edge/internal/service/model_queue.go`
- `apps/edge/internal/service/status_provider_test.go`

### 테스트 환경 규칙

- test_env: `local`.
- `agent-test/local/rules.md`를 읽었다. Go quick check는 현재 local checkout에서 실행하고, Docker compose가 필요한 검증은 원격 runner가 필요하다는 규칙을 확인했다.
- 매칭 profile: `agent-test/local/control-plane-smoke.md`를 읽었다. 적용 명령은 `go test ./apps/control-plane/...`다.
- 참고 profile: `agent-test/local/platform-common-smoke.md`를 읽었다. 이번 plan은 proto 원본/생성물 변경 없이 기존 generated field를 소비하므로 `make proto`는 요구하지 않는다.
- HTTP lifecycle 자체를 새 endpoint로 확장하지 않고 기존 `/edges/{id}/status` JSON DTO를 보강한다. `/healthz`, `/readyz`, `make test-control-plane-edge-wire`는 smoke 후보지만 이 plan의 필수 최종 검증은 대상 Go 패키지 테스트로 둔다.

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

- 현재 `TestEdgeRegistryHTTPHandlersGetLiveStatus`는 node config summary와 secret non-leak만 검증하고 provider snapshots JSON을 검증하지 않는다.
- `edgeStatusResponseViewFromProto`가 `EdgeNodeSnapshot.ProviderSnapshots`를 누락해도 현재 테스트가 실패하지 않는다.
- Edge internal status coverage는 `apps/edge/internal/service/status_provider_test.go`에서 provider `Capacity`, `InFlight`, `Queued`, `LoadRatio`까지 검증되어 있다. 이번 plan은 Control Plane JSON 변환의 회귀 테스트만 추가한다.

### 심볼 참조

- renamed/removed symbols: none.
- 새 internal DTO 후보: `providerSnapshotView`.
- 변경 call site: `edgeStatusResponseViewFromProto` 내부 node mapping only.

### 분할 판단

- split decision policy를 평가했다.
- 공유 task group: `agent-task/m-provider-catalog-device-status/`.
- `03_catalog_status_http`: Control Plane HTTP DTO/API test. dependency: none.
- `04+03_client_catalog_view`: Client DTO/UI 소비. dependency: `03_catalog_status_http` complete.log.
- 분할 이유: Control Plane JSON 계약이 Client UI의 입력 계약이므로 API surface를 먼저 닫고, Client는 그 JSON shape에 의존해 별도 검증한다. Go HTTP tests와 Flutter widget tests는 서로 다른 runner/리스크를 가진다.

### 범위 결정 근거

- 포함: `apps/control-plane/cmd/control-plane/http_views.go`, `apps/control-plane/cmd/control-plane/main_test.go`.
- 제외: `proto/iop/*.proto`와 `proto/gen/**`. 이미 `EdgeNodeSnapshot.provider_snapshots`와 `ProviderSnapshot` generated getters가 있으므로 계약 변경이 아니다.
- 제외: Edge queue/status 계산. `apps/edge/internal/service/model_queue.go`와 status tests가 이미 provider stats를 만든다.
- 제외: Client DTO/UI. dependent plan `04+03_client_catalog_view`에서 처리한다.
- 제외: runtime control, resource telemetry, qualification report. SDD D05-D06과 Milestone 범위 제외에 따라 후속 범위다.

### 빌드 등급

- lane/grade: `local-G04`.
- 근거: 단일 Go app HTTP DTO 변환과 대상 테스트 보강이다. 기존 proto/generated 타입이 있어 public wire contract 변경은 없고, `go test ./apps/control-plane/...`로 결정적으로 검증 가능하다.

## 구현 체크리스트

- [ ] Control Plane HTTP status DTO에 provider snapshot read-only fields를 추가하고 `EdgeNodeSnapshot.ProviderSnapshots`를 JSON으로 전달한다.
- [ ] `/edges/{id}/status` 테스트에 provider snapshot fixture와 JSON assertion을 추가해 `health`, `capacity`, `in_flight`, `load_ratio`, `served_models`, `queued`, `category`, `lifecycle_capabilities`가 보존되는지 확인한다.
- [ ] raw config secret non-leak 검증이 provider snapshot 추가 후에도 유지되는지 확인한다.
- [ ] `go test ./apps/control-plane/...`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] Provider Snapshot JSON DTO 추가

#### 문제

[apps/control-plane/cmd/control-plane/http_views.go](/config/workspace/iop/apps/control-plane/cmd/control-plane/http_views.go:138)에서 `edgeNodeSnapshotView`가 `NodeID`, `Alias`, `Label`, `Connected`, `Config`만 가진다. [apps/control-plane/cmd/control-plane/http_views.go](/config/workspace/iop/apps/control-plane/cmd/control-plane/http_views.go:191)의 `edgeStatusResponseViewFromProto`도 `n.GetProviderSnapshots()`를 읽지 않아 Edge가 보낸 provider status가 HTTP JSON에서 사라진다.

현재 코드:

```go
138 type edgeNodeSnapshotView struct {
139     NodeID    string                 `json:"node_id"`
140     Alias     string                 `json:"alias"`
141     Label     string                 `json:"label"`
142     Connected bool                   `json:"connected"`
143     Config    *nodeConfigSummaryView `json:"config,omitempty"`
144 }
...
191 func edgeStatusResponseViewFromProto(resp *iop.EdgeStatusResponse) edgeStatusResponseView {
192     nodes := make([]edgeNodeSnapshotView, 0, len(resp.GetNodes()))
193     for _, n := range resp.GetNodes() {
194         nodes = append(nodes, edgeNodeSnapshotView{
195             NodeID:    n.GetNodeId(),
196             Alias:     n.GetAlias(),
197             Label:     n.GetLabel(),
198             Connected: n.GetConnected(),
199             Config:    newNodeConfigSummaryView(n.GetConfig()),
200         })
201     }
```

#### 해결 방법

`providerSnapshotView`를 `http_views.go`에 추가하고 `edgeNodeSnapshotView`에 `ProviderSnapshots []providerSnapshotView 'json:"provider_snapshots,omitempty"'`를 둔다. 변환 helper는 proto getters만 사용하고 nil/empty는 nil을 반환한다.

목표 코드:

```go
type providerSnapshotView struct {
    Adapter               string   `json:"adapter,omitempty"`
    Status                string   `json:"status,omitempty"`
    Capacity              int32    `json:"capacity"`
    InFlight              int32    `json:"in_flight"`
    Queued                int32    `json:"queued"`
    ID                    string   `json:"id,omitempty"`
    Type                  string   `json:"type,omitempty"`
    Category              string   `json:"category,omitempty"`
    ServedModels          []string `json:"served_models,omitempty"`
    Health                string   `json:"health,omitempty"`
    LoadRatio             float32  `json:"load_ratio,omitempty"`
    LifecycleCapabilities []string `json:"lifecycle_capabilities,omitempty"`
}

func providerSnapshotViews(snaps []*iop.ProviderSnapshot) []providerSnapshotView {
    if len(snaps) == 0 {
        return nil
    }
    views := make([]providerSnapshotView, 0, len(snaps))
    for _, p := range snaps {
        views = append(views, providerSnapshotView{
            Adapter:               p.GetAdapter(),
            Status:                p.GetStatus(),
            Capacity:              p.GetCapacity(),
            InFlight:              p.GetInFlight(),
            Queued:                p.GetQueued(),
            ID:                    p.GetId(),
            Type:                  p.GetType(),
            Category:              p.GetCategory(),
            ServedModels:          append([]string(nil), p.GetServedModels()...),
            Health:                p.GetHealth(),
            LoadRatio:             p.GetLoadRatio(),
            LifecycleCapabilities: append([]string(nil), p.GetLifecycleCapabilities()...),
        })
    }
    return views
}
```

`edgeStatusResponseViewFromProto`의 node append에는 `ProviderSnapshots: providerSnapshotViews(n.GetProviderSnapshots())`를 추가한다.

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

- [ ] `apps/control-plane/cmd/control-plane/http_views.go`: DTO struct와 helper 추가.
- [ ] `apps/control-plane/cmd/control-plane/http_views.go`: `edgeStatusResponseViewFromProto` node mapping에 provider snapshots 추가.
- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: JSON shape assertion 추가.

#### 테스트 작성

- 작성: `apps/control-plane/cmd/control-plane/main_test.go`의 `TestEdgeRegistryHTTPHandlersGetLiveStatus/Success`에 provider snapshot fixture와 typed/raw assertion을 추가한다.

#### 중간 검증

```bash
go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1
```

기대 결과: provider snapshot JSON assertion과 기존 secret non-leak assertion이 모두 통과한다.

### [API-2] Status JSON 회귀 테스트 보강

#### 문제

[apps/control-plane/cmd/control-plane/main_test.go](/config/workspace/iop/apps/control-plane/cmd/control-plane/main_test.go:444)의 fixture는 `EdgeStatusResponse.Nodes[0]`에 config만 넣고 provider snapshots를 넣지 않는다. 그래서 `provider_snapshots` 누락이 회귀로 잡히지 않는다.

현재 fixture 일부:

```go
444 return &iop.EdgeStatusResponse{
445     RequestId:            "req-123",
446     EdgeId:               "edge-a",
447     EdgeName:             "Edge A",
448     ObservedTimeUnixNano: connectedAt.UnixNano(),
449     Nodes: []*iop.EdgeNodeSnapshot{
450         {
451             NodeId:    "node-1",
452             Alias:     "alpha",
453             Label:     "node0",
454             Connected: true,
455             Config: &iop.NodeConfigPayload{
```

#### 해결 방법

fixture에 provider snapshot을 추가하고 typed DTO와 raw JSON 모두 확인한다. raw assertion은 `provider_snapshots` 키가 있고 secret/config internals가 들어가지 않는다는 기존 검증과 공존해야 한다.

목표 fixture:

```go
ProviderSnapshots: []*iop.ProviderSnapshot{
    {
        Adapter:               "vllm-gpu",
        Status:                "available",
        Capacity:              4,
        InFlight:              1,
        Queued:                2,
        Id:                    "provider-vllm-primary",
        Type:                  "vllm",
        Category:              "api",
        ServedModels:          []string{"nvidia/Qwen3.6-35B-A3B-NVFP4"},
        Health:                "available",
        LoadRatio:             0.25,
        LifecycleCapabilities: []string{"list_models", "load_model"},
    },
},
```

Assert `node.ProviderSnapshots[0]` preserves all fields, and raw JSON has `provider_snapshots[0].load_ratio == 0.25`, `served_models[0]`, `lifecycle_capabilities[0]`.

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

- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: fixture에 provider snapshot 추가.
- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: typed DTO assertion 추가.
- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: raw JSON key assertion 추가.

#### 테스트 작성

- 작성: 위 test에 직접 추가한다. 별도 새 test보다 기존 status handler 성공 케이스에 붙이는 편이 HTTP view 계약을 한 곳에서 검증한다.

#### 중간 검증

```bash
go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1
```

기대 결과: status handler 성공/누락/502 subtest가 모두 통과한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/control-plane/cmd/control-plane/http_views.go` | API-1 |
| `apps/control-plane/cmd/control-plane/main_test.go` | API-2 |

## 최종 검증

```bash
go test ./apps/control-plane/... -count=1
git diff --check
```

기대 결과: Go control-plane tests가 통과하고 공백 오류가 없다. Go test cache output은 허용하지 않는다. 모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
