<!-- task=m-provider-resource-admission-ownership-alignment/04_provider_pool_policy_config plan=1 tag=REVIEW_PROVIDER_POLICY_CONFIG -->

# Provider-pool Policy Loader 경계 수정 계획

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

구현과 검증을 완료한 뒤 `CODE_REVIEW-local-G04.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 stdout/stderr로 채우고 active 파일을 그대로 둔 채 리뷰 준비 상태를 보고한다. 종결 판정, log rename, `complete.log`, archive 이동은 code-review 전용이다. 선택된 Milestone의 `구현 잠금 > 결정 필요`가 구현을 막을 때만 review stub의 `사용자 리뷰 요청`에 정확한 연결 근거를 기록하고 멈춘다. 직접 사용자 질문·선택지·`request_user_input`, `USER_REVIEW.md`, archive log, `complete.log` 작성은 금지한다. 환경/secret/service blocker, 일반 범위 조정, 후속 에이전트가 해소할 수 있는 evidence gap은 사용자 리뷰 요청이 아니다.

## 배경

첫 리뷰에서 root canonical 음수와 legacy timeout-only migration이 계획의 normalization table과 다르게 처리되는 것을 확인했다. 기존 검증은 통과했지만 두 경계를 테스트하지 않아 잘못된 effective policy를 허용했다. 이 후속 작업은 loader와 회귀 테스트만 좁게 수정한다.

## 사용자 리뷰 요청 흐름

선택된 Milestone lock 결정만 active `CODE_REVIEW-local-G04.md`의 `사용자 리뷰 요청` 템플릿에 기록한다. 구현 중 직접 사용자 prompt는 금지하며 code-review가 검증과 실제 `USER_REVIEW.md` 작성을 소유한다.

## Archive Evidence Snapshot

- Prior task: `agent-task/m-provider-resource-admission-ownership-alignment/04_provider_pool_policy_config`
- Archived plan: `agent-task/m-provider-resource-admission-ownership-alignment/04_provider_pool_policy_config/plan_local_G06_0.log`
- Archived review: `agent-task/m-provider-resource-admission-ownership-alignment/04_provider_pool_policy_config/code_review_local_G06_0.log`
- Verdict: `FAIL`
- Findings: Required 2, Suggested 0, Nit 1(review에서 수정 완료)
- Required summary:
  - `packages/go/config/load.go:193`에서 canonical negative `max_queue`가 기본값으로 덮여 validation을 우회한다.
  - `packages/go/config/load.go:229`에서 legacy partial pair를 raw 값으로 비교·승격해 timeout-only policy가 `max_queue: 0`이 되고 의미가 같은 pair도 충돌할 수 있다.
- Affected files: `packages/go/config/load.go`, `packages/go/config/provider_catalog_config_test.go`
- Verification evidence: fresh targeted tests, `go test -count=1 ./packages/go/...`, `go test -count=1 ./apps/edge/...`, `edge config check`, `gofmt -d`, `git diff --check`는 PASS했다. 별도 CLI reproduction에서 canonical `max_queue: -1`은 `16`/exit 0, legacy timeout-only는 `max_queue: 0`으로 출력되었다.
- Reviewer repair: `configs/edge.yaml` recommended provider-pool 예시의 legacy queue fields를 제거하고 설명을 정리했다.
- Roadmap carryover: SDD S06 / `queue-ownership`의 schema·compatibility 부분 작업이며 이 subtask PASS만으로 Milestone Task 완료를 주장하지 않는다.
- Allowed narrow reread: 위 archived plan/review 두 파일만 필요할 때 다시 읽을 수 있다.

## 분석 결과

### 읽은 파일

- 규칙/워크플로: `agent-ops/rules/project/rules.md`, `agent-ops/rules/common/rules-roadmap.md`, `agent-ops/rules/common/rules-agent-spec.md`, `agent-ops/rules/project/domain/platform-common/rules.md`, `agent-ops/rules/project/domain/edge/rules.md`, `agent-ops/rules/project/domain/testing/rules.md`, `agent-ops/skills/common/router.md`, `agent-ops/skills/common/code-review/SKILL.md`, `agent-ops/skills/common/plan/SKILL.md`
- 테스트 규칙: `agent-test/local/rules.md`, `agent-test/local/platform-common-smoke.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/testing-smoke.md`
- 로드맵/SDD: `agent-roadmap/current.md`, `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-resource-admission-ownership-alignment.md`, `agent-roadmap/sdd/operational-observability-provider-management/provider-resource-admission-ownership-alignment/SDD.md`
- 계약/스펙: `agent-contract/index.md`, `agent-contract/inner/edge-config-runtime-refresh.md`, `agent-spec/index.md`, `agent-spec/runtime/provider-pool-config-refresh.md`
- 설정 소스: `packages/go/config/edge_types.go`, `packages/go/config/provider_types.go`, `packages/go/config/load.go`, `packages/go/config/validate.go`
- refresh/runtime 연결: `apps/edge/internal/configrefresh/classify.go`, `request.go`, `result.go`, `apps/edge/internal/bootstrap/runtime.go`, `apps/edge/internal/service/service.go`, `model_queue_types.go`, `model_queue_admission.go`, `provider_resolution.go`
- 테스트/가이드: `packages/go/config/provider_catalog_config_test.go`, `packages/go/config/provider_catalog_validation_config_test.go`, `apps/edge/internal/configrefresh/provider_classify_test.go`, `configs/edge.yaml`, `apps/edge/README.md`
- 이전 루프 evidence: 위 `Archive Evidence Snapshot`의 archived plan/review

### SDD 기준

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-resource-admission-ownership-alignment/SDD.md`, 상태 `[승인됨]`, 잠금 `해제`.
- Acceptance Scenario S06 / Task `queue-ownership`은 root canonical owner, 동일한 legacy effective 값의 승격, 충돌 거부와 first-candidate 금지를 요구한다.
- Evidence Map의 S06 deterministic config evidence 중 boundary normalization 회귀를 이 후속 체크리스트와 fresh config/package 검증에 반영한다. admission 전체 evidence는 후속 subtask가 소유한다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`, `platform-common-smoke.md`, `edge-smoke.md`, `testing-smoke.md`를 읽었다.
- runner `/config/workspace/iop`, branch `main`, HEAD `e4966b0b173d`, Linux aarch64, Go 1.26.2이며 dirty worktree를 보존한다.
- fresh `go test -count=1`을 사용한다. 변경 package, `packages/go/...`, 소비 Edge package 전체, 실제 `edge config check`를 실행한다.
- 외부 provider, Docker, secret은 필요 없다.

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

- canonical `provider_pool.max_queue < 0` rejection 테스트가 없어 음수 validation 우회를 놓쳤다.
- legacy timeout-only pair의 default max normalization과 raw 표현이 다른 effective-equal pair 테스트가 없다.
- canonical timeout 음수는 구현이 거부하지만 새 public schema boundary를 table test로 고정하지 않았다.

### 심볼 참조

- rename/remove 없음.
- `resolveProviderPoolPolicy`는 `LoadEdge` 내부 전용 helper이며 호출 위치는 `packages/go/config/load.go` 한 곳이다.

### 분할 판단

- 기존 split subtask `04_provider_pool_policy_config`의 동일 loader/test 경계 후속이다.
- 두 Required는 같은 helper의 validation/normalization 순서와 같은 targeted suite를 공유하므로 추가 subtask 분리는 인위적이다.

### 범위 결정 근거

- `apps/edge/internal/service/**` admission, runtime refresh propagation, contract/spec sync는 각각 후속 subtask/Milestone Task 범위이므로 수정하지 않는다.
- reviewer가 고친 `configs/edge.yaml` 비동작 문서 변경은 보존하고 추가 문서 범위를 확장하지 않는다.
- `NodeProviderConf` compatibility fields와 adapter-local queue fields는 제거하지 않는다.

### 빌드 등급

- `local-G04`: 두 명시적 loader boundary와 deterministic unit/config 검증만 필요한 bounded bug fix다.

## 구현 체크리스트

- [ ] canonical `provider_pool.max_queue` 음수를 default 적용 전에 거부하고 zero-only default semantics를 canonical negative regression test로 고정한다.
- [ ] legacy promotion 후보를 effective pair로 정규화한 뒤 비교·승격하고 timeout-only/effective-equal regression tests를 추가한다.
- [ ] fresh targeted, `packages/go/...`, `apps/edge/...`, config check 검증을 통과시킨다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_PROVIDER_POLICY_CONFIG-1] Canonical 음수 validation 순서 수정

#### 문제

`packages/go/config/load.go:193-203`은 `MaxQueue <= 0`을 기본값으로 덮어쓴 뒤 `< 0`을 검사해 음수 입력을 허용한다.

#### 해결 방법

Before:

```go
if cfg.ProviderPool.MaxQueue <= 0 {
	cfg.ProviderPool.MaxQueue = DefaultProviderPoolMaxQueue
}
if cfg.ProviderPool.MaxQueue < 0 {
	return fmt.Errorf("provider_pool.max_queue must be non-negative")
}
```

After:

```go
if cfg.ProviderPool.MaxQueue < 0 {
	return fmt.Errorf("provider_pool.max_queue must be non-negative")
}
if cfg.ProviderPool.MaxQueue == 0 {
	cfg.ProviderPool.MaxQueue = DefaultProviderPoolMaxQueue
}
```

timeout 음수도 같은 canonical boundary table에서 명시적으로 고정한다.

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

- [ ] `packages/go/config/load.go`: canonical negative check를 zero default보다 먼저 수행한다.
- [ ] `packages/go/config/provider_catalog_config_test.go`: canonical max/timeout negative rejection subtests를 추가한다.

#### 테스트 작성

- `TestLoadEdge_ProviderPoolPolicyCanonicalNegativeRejected`: `max_queue: -1`, `queue_timeout_ms: -1` 각각 error와 field name을 검증한다.

#### 중간 검증

```bash
gofmt -d packages/go/config/load.go packages/go/config/provider_catalog_config_test.go
GOCACHE=/tmp/iop-go-cache-provider-policy-followup go test -count=1 ./packages/go/config -run 'TestLoadEdge_ProviderPoolPolicyCanonicalNegativeRejected'
```

기대 결과: gofmt diff 없음, 두 negative subtest PASS.

### [REVIEW_PROVIDER_POLICY_CONFIG-2] Legacy effective pair normalization

#### 문제

`packages/go/config/load.go:229-258`은 legacy raw pair를 그대로 비교·승격한다. timeout-only provider는 `(0, timeout)`이 아니라 계획 표의 `(16, timeout)`으로 정규화되어야 한다.

#### 해결 방법

legacy candidate 참여 여부는 raw 값으로 먼저 판정하되, 참여하는 pair의 `max_queue == 0`은 `DefaultProviderPoolMaxQueue`로 바꾼 뒤 equality 비교와 promotion에 사용한다. `max_queue > 0`과 timeout `0` 조합은 no-timeout semantics를 그대로 보존한다.

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

- [ ] `packages/go/config/load.go`: participating legacy pair를 effective value로 정규화한 뒤 비교·승격한다.
- [ ] `packages/go/config/provider_catalog_config_test.go`: timeout-only promotion, effective-equal raw pair, max-only no-timeout cases를 table test로 추가한다.

#### 테스트 작성

- `TestLoadEdge_ProviderPoolPolicyLegacyPartialFields`: timeout-only → `(16, timeout)`, max-only → `(max, 0)`을 검증한다.
- `TestLoadEdge_ProviderPoolPolicyLegacyEffectiveEquality`: `(0, timeout)`과 `(16, timeout)`이 충돌 없이 `(16, timeout)`으로 승격되는지 검증한다.

#### 중간 검증

```bash
gofmt -d packages/go/config/load.go packages/go/config/provider_catalog_config_test.go
GOCACHE=/tmp/iop-go-cache-provider-policy-followup go test -count=1 ./packages/go/config -run 'TestLoadEdge_ProviderPoolPolicy(LegacyPartialFields|LegacyEffectiveEquality)'
```

기대 결과: timeout-only/default/no-timeout/effective equality regression PASS.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `packages/go/config/load.go` | REVIEW_PROVIDER_POLICY_CONFIG-1, REVIEW_PROVIDER_POLICY_CONFIG-2 |
| `packages/go/config/provider_catalog_config_test.go` | REVIEW_PROVIDER_POLICY_CONFIG-1, REVIEW_PROVIDER_POLICY_CONFIG-2 |

## 최종 검증

```bash
gofmt -d packages/go/config/load.go packages/go/config/provider_catalog_config_test.go
git diff --check
GOCACHE=/tmp/iop-go-cache-provider-policy-followup go test -count=1 ./packages/go/config -run 'TestLoadEdge_ProviderPoolPolicy'
GOCACHE=/tmp/iop-go-cache-provider-policy-followup go test -count=1 ./packages/go/...
GOCACHE=/tmp/iop-go-cache-provider-policy-followup go test -count=1 ./apps/edge/...
GOCACHE=/tmp/iop-go-cache-provider-policy-followup go run ./apps/edge/cmd/edge --config configs/edge.yaml config check
```

기대 결과: format/diff/targeted/platform-common/Edge/config check 모두 PASS. fresh execution만 허용한다.

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