<!-- task=m-node-provider-first-config-surface/02+01_normalize_compile plan=0 tag=COMPILE -->

# Plan - COMPILE

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

구현 마지막 단계는 `CODE_REVIEW-local-G06.md` 작성이다. 구현 중 Milestone lock 결정이 새로 막으면 review stub의 `사용자 리뷰 요청`에만 기록하고 멈춘다. finalization은 code-review 전용이다.

## 배경

현재 `BuildConfigPayload`는 `NodeRecord.Adapters`에서만 `NodeConfigPayload.adapters`를 생성한다. SDD S02는 provider-first config를 internal adapter registry와 CLI profile set으로 컴파일하고, provider id가 기본 adapter instance key가 되도록 요구한다.

## 사용자 리뷰 요청 흐름

선택된 Milestone lock 결정만 review stub에 기록한다. 구현 중 일반 설계 미세 조정은 `계획 대비 변경 사항`에 남긴다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-provider-first-config-surface.md`
- Task ids:
  - `normalize-compile`: provider-first config compile to adapter registry and NodeConfigPayload
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/sdd/operational-observability-provider-management/node-provider-first-config-surface/SDD.md`
- `agent-contract/inner/edge-node-runtime-wire.md`
- `agent-contract/inner/edge-config-runtime-refresh.md`
- `apps/edge/internal/node/mapper.go`
- `apps/edge/internal/node/store.go`
- `apps/node/internal/adapters/config_set.go`
- `apps/node/internal/router/router.go`
- `proto/iop/runtime.proto`
- `agent-test/local/rules.md`
- `agent-test/local/node-smoke.md`
- `agent-test/local/edge-smoke.md`

### SDD 기준

대상 Acceptance Scenario는 S02다. Evidence Map은 `mapper/config_set/router tests proving provider-derived adapter payload`를 요구한다. `NodeConfigPayload.adapters` proto는 이미 typed oneof를 제공하므로 proto 변경 없이 provider resource를 adapter config로 컴파일하는 쪽을 우선한다.

### 테스트 환경 규칙

`test_env=local`. `node-smoke`와 `edge-smoke` 기준을 읽었다. 적용 명령은 `go test ./apps/edge/internal/node ./apps/node/internal/adapters ./apps/node/internal/router`; 실행 파이프라인에 닿으므로 보조로 `go test ./...`를 둔다.

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

provider-derived adapter payload 테스트가 없다. `apps/edge/internal/node/mapper.go:32`는 `rec.Adapters.*Instances`만 순회하고 `rec.Providers`를 compile source로 쓰지 않는다.

### 심볼 참조

rename/remove 없음. `BuildConfigPayload`, `BuildConfigSet`, router `Lookup` 경로가 주요 참조다.

### 분할 판단

SDD sibling task 중 이 plan은 compile layer만 다룬다. `02+01_normalize_compile`은 `01_schema_source` 완료 후 실행한다. dispatch/status는 `04+02,03_routing_status_refresh` 범위다.

### 범위 결정 근거

OpenAI handler와 queue selection은 제외한다. 이 plan은 provider config가 Node runtime에 도달할 수 있는 IR compile까지만 닫는다.

### 빌드 등급

`local-G06`: Edge mapper와 Node adapter registry를 함께 건드리지만 내부 계약이 명확하고 package tests로 검증 가능하다.

## 의존 관계 및 구현 순서

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

## 구현 체크리스트

- [ ] `BuildConfigPayload`가 `rec.Providers`에서 provider id 기반 adapter config를 생성하도록 한다.
- [ ] CLI provider는 `CLIAdapterConfig.Profiles[provider.id]` 또는 provider id target으로 컴파일하고, OpenAI-compatible/Ollama provider는 typed adapter oneof로 컴파일한다.
- [ ] provider-derived adapter key가 Node router에서 exact instance key로 resolve되는 테스트를 추가한다.
- [ ] `go test ./apps/edge/internal/node ./apps/node/internal/adapters ./apps/node/internal/router`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [COMPILE-1] Provider-Derived Adapter Payload

#### 문제

`apps/edge/internal/node/mapper.go:32`는 adapter instances에서만 payload를 만든다. provider-first config에서 `providers[].adapter`가 비어 있으면 Node는 provider id adapter key를 받을 수 없다.

#### 해결 방법

`providers[]`를 먼저 internal adapter config로 compile한다. provider id를 `AdapterConfig.Name`에 넣고 provider `type`으로 `AdapterConfig.Type`을 정한다. legacy `adapter`가 있으면 compat override로 유지한다. 동일 key가 legacy adapter instance와 충돌하면 명확한 error를 반환한다.

Before:

```go
// apps/edge/internal/node/mapper.go:32
for _, inst := range rec.Adapters.OllamaInstances {
```

After:

```go
for _, provider := range rec.Providers {
    ac, err := providerToAdapterConfig(provider)
    if err != nil { return nil, err }
    payload.Adapters = append(payload.Adapters, ac)
}
```

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

- [ ] `apps/edge/internal/node/mapper.go`: provider-to-adapter compiler 추가.
- [ ] `apps/edge/internal/node/mapper_test.go`: openai_compat, ollama, cli provider compile tests 추가.
- [ ] `apps/node/internal/adapters/config_set_test.go`: provider id name으로 registry item 생성 확인.
- [ ] `apps/node/internal/router/router_test.go`: provider id exact key resolve 확인.

#### 테스트 작성

작성한다. `TestBuildConfigPayload_ProviderFirstOpenAICompat`, `TestBuildConfigPayload_ProviderFirstCLIProfile`, `TestRouterResolvesProviderIDInstanceKey`를 추가한다.

#### 중간 검증

```bash
go test ./apps/edge/internal/node ./apps/node/internal/adapters ./apps/node/internal/router
```

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/node/mapper.go` | COMPILE-1 |
| `apps/edge/internal/node/mapper_test.go` | COMPILE-1 |
| `apps/node/internal/adapters/config_set_test.go` | COMPILE-1 |
| `apps/node/internal/router/router_test.go` | COMPILE-1 |

## 최종 검증

```bash
go test ./apps/edge/internal/node ./apps/node/internal/adapters ./apps/node/internal/router
```

```bash
go test ./apps/node/... ./apps/edge/internal/node
```

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