<!-- task=m-provider-resource-admission-ownership-alignment/07+03_snapshot_source plan=2 tag=REVIEW_REVIEW_PROVIDER_SNAPSHOT_SOURCE -->

# Provider Snapshot Production Surface와 Race Evidence 보완 계획

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

구현과 검증을 완료한 뒤 `CODE_REVIEW-local-G06.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은 사용자 리뷰 요청이 아니다.

## 배경

두 번째 리뷰에서 actual waiter와 normal/long fixture는 추가됐지만 production `ListNodeSnapshots`와 live resolver를 우회해 SDD S07을 검증하지 못했다. concurrent test는 실제 `providerPoolPolicy` data race를 발견했으나 review artifact에는 성공처럼 기록됐고, 이전 resolver error/empty 회귀도 제거됐다. 이 후속은 runtime snapshot 동기화와 production snapshot lifecycle/race evidence를 같은 bounded scope에서 닫는다.

## 사용자 리뷰 요청 흐름

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

## Archive Evidence Snapshot

- Prior task path: `agent-task/m-provider-resource-admission-ownership-alignment/07+03_snapshot_source`
- Archived plan: `agent-task/m-provider-resource-admission-ownership-alignment/07+03_snapshot_source/plan_local_G06_1.log`
- Archived review: `agent-task/m-provider-resource-admission-ownership-alignment/07+03_snapshot_source/code_review_local_G06_1.log`
- Verdict: FAIL
- Findings: Required 4, Suggested 0, Nit 0.
  - `runtimeConfigSnapshot`의 queue policy 읽기와 runtime apply 쓰기가 실제 Go data race를 만든다.
  - lifecycle test가 live resolver를 nil로 지우고 내부 queue/resource map을 직접 읽어 production snapshot surface를 우회한다.
  - test helper가 `long_in_flight`와 `long_queued`를 합쳐 독립 전이를 검증하지 않는다.
  - `TestProviderSnapshotResolverErrorDoesNotUseStaleCandidate`가 제거됐지만 정규식이 zero-match로 성공했다.
- Affected files: `apps/edge/internal/service/service.go`, `apps/edge/internal/service/status_provider_test.go`.
- Verification evidence:
  - fresh targeted provider snapshot tests: PASS, `ok iop/apps/edge/internal/service 0.326s`; 단, resolver error test는 목록에 존재하지 않았다.
  - `go test -list '^TestProviderSnapshot'`: `TestProviderSnapshotResolverErrorDoesNotUseStaleCandidate` 부재 확인.
  - `GOCACHE=/tmp/iop-go-cache-review-snapshot-g06 go test -race -count=3 -timeout 60s ./apps/edge/internal/service -run 'TestProviderSnapshot(LiveCandidatePressureFollowsRefreshAcrossModelGroups|ConcurrentRefreshPressureUsesLiveCandidates|QueuedCountsCandidatePressureAcrossModelGroups)$'`: FAIL; `service.go:106` read와 `model_queue_types.go:269` write data race, exit 1.
  - `gofmt -d apps/edge/internal/service/status_provider_test.go`, `git diff --check`: PASS, 출력 없음.
- Roadmap carryover: `snapshot-source`는 actual `ListNodeSnapshots`의 일반·long lifecycle, live resolver error/empty, concurrent refresh race PASS evidence가 모두 충족될 때만 완료 대상으로 유지한다.
- Narrow reread allowed: 위 `plan_local_G06_1.log`, `code_review_local_G06_1.log` 두 파일만 필요 시 읽는다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-resource-admission-ownership-alignment.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/operational-observability-provider-management/milestones/provider-resource-admission-ownership-alignment.md)
- Task ids:
  - `snapshot-source`: provider lease state와 live candidate pressure 기반 snapshot
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- 규칙/라우팅: `agent-ops/rules/project/rules.md`, `agent-ops/rules/common/rules-roadmap.md`, `agent-ops/skills/common/router.md`, `agent-ops/skills/common/code-review/SKILL.md`, `agent-ops/skills/common/plan/SKILL.md`
- 이전 루프: `agent-task/m-provider-resource-admission-ownership-alignment/07+03_snapshot_source/plan_local_G05_0.log`, `code_review_local_G05_0.log`, `plan_local_G06_1.log`, `code_review_local_G06_1.log`
- 로드맵/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-spec/runtime/provider-pool-config-refresh.md`, `agent-spec/control/control-plane-operations.md`, `agent-contract/inner/edge-config-runtime-refresh.md`, `agent-contract/inner/control-plane-edge-wire.md`
- source: `apps/edge/internal/service/service.go`, `apps/edge/internal/service/model_queue_snapshot.go`, `apps/edge/internal/service/model_queue_types.go`, `apps/edge/internal/service/model_queue_admission.go`, `apps/edge/internal/service/status_provider.go`
- tests: `apps/edge/internal/service/status_provider_test.go`
- 테스트 규칙/프로필: `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/testing-smoke.md`
- 선행 완료: `agent-task/archive/2026/07/m-provider-resource-admission-ownership-alignment/03+01,02_cross_group_wakeup/complete.log`

### SDD 기준

- SDD는 `agent-roadmap/sdd/operational-observability-provider-management/provider-resource-admission-ownership-alignment/SDD.md`, 상태 `[승인됨]`, 잠금 해제다.
- 대상은 Acceptance Scenario S07 / Task `snapshot-source`다.
- S07은 실제 snapshot 조회에서 일반·long in-flight가 lease state와 같고 queued/long_queued가 live candidate pressure임을 요구한다.
- Evidence Map의 S07 row가 deterministic Edge provider resource/queue tests와 race 검증을 요구하므로 production `ListNodeSnapshots`, resolver error/empty, normal/long terminal lifecycle, concurrent refresh race PASS를 구현 체크리스트와 최종 검증에 고정한다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`, `edge-smoke.md`, `testing-smoke.md`를 읽었다.
- fresh Go test는 별도 `/tmp` GOCACHE와 `-count=1`을 사용하며 race 검증은 `-race -count=5`로 실행한다. cached output은 완료 evidence로 허용하지 않는다.
- runner는 `/config/workspace/iop`, branch `main`, HEAD `e4966b0`, Linux aarch64이고 worktree는 병렬 sibling 변경으로 dirty다. 기존 변경을 되돌리지 않고 두 대상 파일의 최소 diff만 추가한다.
- provider-pool preflight는 이전 두 루프에서 `/models`와 Control Plane status endpoint 접근 불가로 rc=3이었다. 같은 명령을 재실행해 실제 blocker stdout/stderr와 `/tmp` evidence 경로를 기록하되 deterministic S07 검증 실패를 성공으로 바꾸지 않는다.

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

- 실제 `ListNodeSnapshots`의 lifecycle field 전이를 읽는 테스트가 없다.
- live `resolveCandidates`를 보존한 waiter dispatch/refresh snapshot 테스트가 없다.
- `long_in_flight`와 `long_queued`를 독립적으로 non-zero/zero 전이시키는 assertion이 없다.
- resolver error/empty에서 stale enqueue candidate를 제외하는 dedicated test가 제거됐다.
- runtime store/catalog/policy snapshot과 queue reconcile의 동시 refresh/status read가 data-race-free라는 evidence가 없다.

### 심볼 참조

- rename/remove 대상은 없다.
- `runtimeConfigSnapshot` 호출부는 `service.go`, `provider_resolution.go`, `status_provider.go`에 있으며 시그니처는 유지한다.

### 분할 판단

- split decision policy를 평가했다. runtime snapshot lock correction과 snapshot lifecycle/race test는 같은 S07 failure를 닫고 동일 service shared-state를 검증하므로 분리하면 race 재현 fixture와 검증 계약이 중복된다.
- 기존 split subtask `07+03_snapshot_source`를 유지한다. predecessor `03`은 `agent-task/archive/2026/07/m-provider-resource-admission-ownership-alignment/03+01,02_cross_group_wakeup/complete.log`로 충족됐다.
- `service.go`에는 active sibling 05/06 변경이 있으므로 이를 되돌리지 않고 현재 baseline 위에 policy read/reconcile 동기화만 최소 적용한다. 별도 hidden dependency는 추가하지 않는다.

### 범위 결정 근거

- `model_queue_snapshot.go`의 live pressure aggregation 의미는 유지하고 이번 후속에서 재설계하지 않는다.
- queue timeout/refresh 정책, config schema, contract/spec/roadmap/SDD 본문, live capacity smoke 성공은 각각 sibling Task와 `capacity-smoke` 범위다.
- race 수정이 store/catalog/policy snapshot 동기화 밖의 API 변경을 요구하면 임의 확장하지 않고 review artifact에 실패 evidence를 남긴다.

### 빌드 등급

- `local-G06`: 한 service snapshot 경계와 한 테스트 파일의 bounded concurrency/test 보완이며 exact unit/race 명령으로 판정 가능하다.

## 의존 관계 및 구현 순서

1. 현재 `service.go`의 sibling 변경을 보존한 채 runtime store/catalog/policy와 queue reconcile lock 순서를 먼저 정합화한다.
2. lifecycle fixture에서 live resolver를 보존하고 모든 phase를 production `ListNodeSnapshots`으로 읽는다.
3. 일반·long field를 독립 assertion하고 resolver error/empty 회귀를 복구한다.
4. fresh targeted/race/Edge 전체 검증과 preflight를 실행한다.

## 구현 체크리스트

- [ ] runtime store/catalog/policy snapshot과 queue reconcile을 일관된 lock order로 동기화해 concurrent refresh/status snapshot data race를 제거한다.
- [ ] actual waiter의 live resolver를 보존하고 모든 lifecycle phase를 production `ListNodeSnapshots`의 provider snapshot으로 검증한다.
- [ ] normal/long case에서 `in_flight`, `queued`, `long_in_flight`, `long_queued`를 각각 독립 assertion하고 terminal 뒤 lease/resource/snapshot zero를 검증한다.
- [ ] resolver error/empty stale-fallback 회귀를 복구하고 exact test existence, fresh targeted/race/Edge 전체 검증과 provider-pool preflight 실제 출력을 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REVIEW_PROVIDER_SNAPSHOT_SOURCE-1] Runtime snapshot과 queue reconcile 동기화

#### 문제

`apps/edge/internal/service/service.go:101-108`은 `s.mu`만 보유한 채 `s.queue.providerPoolPolicy`를 읽고, `SetRuntimeConfig`은 `model_queue_types.go:269`의 policy를 queue lock 없이 갱신한다. concurrent snapshot test의 실제 race detector가 이 read/write를 보고했다.

#### 해결 방법

Service-owned runtime snapshot에 policy를 함께 저장하고, queue manager lock을 먼저 획득한 뒤 Service snapshot을 교체하고 reconcile/pump를 수행해 기존 live resolver의 `m.mu → s.mu` 순서와 맞춘다. `runtimeConfigSnapshot`은 queue 내부 필드를 직접 읽지 않는다.

Before (`service.go:82-108`):

```go
s.mu.Lock()
s.nodeStore = store
s.modelCatalog = catalog
s.mu.Unlock()
s.queue.setProviderPoolPolicyLocked(store, policy)
```

After:

```go
s.queue.mu.Lock()
defer s.queue.mu.Unlock()
s.mu.Lock()
s.nodeStore = store
s.modelCatalog = catalog
s.providerPoolPolicy = policy
s.mu.Unlock()
s.queue.setProviderPoolPolicyLocked(store, policy)
```

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

- [ ] `apps/edge/internal/service/service.go`: Service runtime snapshot에 policy를 포함한다.
- [ ] `apps/edge/internal/service/service.go`: queue/service lock order와 reconcile 호출을 원자적으로 정렬한다.
- [ ] `apps/edge/internal/service/service.go`: `runtimeConfigSnapshot`이 Service-owned policy만 읽도록 한다.

#### 테스트 작성

- 수정: `apps/edge/internal/service/status_provider_test.go`의 concurrent refresh/snapshot race test가 이 경로를 실제 실행한다.
- assertion: race detector 경고 없음, old/new snapshot 내부 field 불변식 유지.

#### 중간 검증

```bash
gofmt -d apps/edge/internal/service/service.go
GOCACHE=/tmp/iop-go-cache-provider-snapshot-followup-2 go test -race -count=5 -timeout 60s ./apps/edge/internal/service -run '^TestProviderSnapshotConcurrentRefreshPressureUsesLiveCandidates$'
```

기대 결과: gofmt diff 없음, race detector 경고 없이 PASS.

### [REVIEW_REVIEW_PROVIDER_SNAPSHOT_SOURCE-2] Production snapshot과 live resolver lifecycle

#### 문제

`apps/edge/internal/service/status_provider_test.go:1027-1035`는 queued item의 `resolveCandidates`를 nil로 지우고 `:983-1079`는 `readSnapshotState`로 내부 queue/resource map을 직접 읽는다. 실제 snapshot surface와 current candidate pressure는 검증되지 않는다.

#### 해결 방법

resolver nil mutation과 `readSnapshotState` 재구현을 제거한다. node/provider id로 실제 `ListNodeSnapshots` 결과를 찾는 read-only helper를 두고 holder, queued, refresh, dispatch, terminal phase마다 반환된 `ProviderSnapshot`을 assertion한다.

Before (`status_provider_test.go:1027-1035`):

```go
svc.queue.mu.Lock()
wg.queue[0].resolveCandidates = nil
svc.queue.mu.Unlock()
```

After:

```go
shared, fallback := requireProviderSnapshots(t, svc.ListNodeSnapshots(), nodeID)
assertProviderSnapshotState(t, shared, ...)
assertProviderSnapshotState(t, fallback, ...)
```

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

- [ ] `apps/edge/internal/service/status_provider_test.go`: resolver nil mutation을 제거한다.
- [ ] `apps/edge/internal/service/status_provider_test.go`: actual `ListNodeSnapshots` lookup/assert helper를 추가한다.
- [ ] `apps/edge/internal/service/status_provider_test.go`: every lifecycle phase에서 production snapshot을 검증한다.

#### 테스트 작성

- 수정: `TestProviderSnapshotLiveCandidatePressureFollowsRefreshAcrossModelGroups`.
- fixture: actual `SubmitRun`, non-nil live resolver, net.Pipe Node client, bounded waiter channel.
- assertion: live current candidates, actual fallback dispatch, terminal drain.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-go-cache-provider-snapshot-followup-2 go test -count=20 -timeout 60s ./apps/edge/internal/service -run '^TestProviderSnapshotLiveCandidatePressureFollowsRefreshAcrossModelGroups$'
```

기대 결과: normal/long actual snapshot lifecycle 20회 PASS.

### [REVIEW_REVIEW_PROVIDER_SNAPSHOT_SOURCE-3] Long in-flight/queued 독립 assertion

#### 문제

`apps/edge/internal/service/status_provider_test.go:746-874`는 `sharedLong`/`fallbackLong`에 long in-flight와 long queued 의미를 합치고 `wantSShort`/`wantFShort` 인자를 사용하지 않는다. queued long request가 non-zero `LongQueued`를 만들었는지 확인할 수 없다.

#### 해결 방법

known-bug dead closure와 `snapshotState` 재구현을 삭제한다. actual protobuf snapshot의 `InFlight`, `Queued`, `LongInFlight`, `LongQueued`를 모두 별도 기대값으로 받는 helper를 사용한다.

Before (`status_provider_test.go:746-754`):

```go
type snapshotState struct {
	sharedInFlight, sharedLong, sharedQueued int
}
```

After:

```go
func assertProviderSnapshotState(t *testing.T, got *iop.ProviderSnapshot,
	inFlight, queued, longInFlight, longQueued int)
```

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

- [ ] `apps/edge/internal/service/status_provider_test.go`: dead/duplicated `readSnapshotState` helper를 제거한다.
- [ ] `apps/edge/internal/service/status_provider_test.go`: 네 snapshot counter를 독립 assertion한다.
- [ ] `apps/edge/internal/service/status_provider_test.go`: normal/long enqueue, dispatch, terminal zero 기대값을 명시한다.

#### 테스트 작성

- 수정: lifecycle test의 `normal`, `long` subtest.
- assertion: long case에서 queued phase `LongInFlight=1`, `LongQueued=1`; dispatch와 terminal에서 독립적으로 0/1 전이.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-go-cache-provider-snapshot-followup-2 go test -race -count=10 -timeout 60s ./apps/edge/internal/service -run '^TestProviderSnapshotLiveCandidatePressureFollowsRefreshAcrossModelGroups$'
```

기대 결과: long field assertion과 race detector 모두 PASS.

### [REVIEW_REVIEW_PROVIDER_SNAPSHOT_SOURCE-4] Resolver error/empty test와 검증 신뢰 복구

#### 문제

`apps/edge/internal/service/status_provider_test.go:540` 이후 snapshot test 목록에 이전 루프의 `TestProviderSnapshotResolverErrorDoesNotUseStaleCandidate`가 없다. Go `-run` 정규식은 일부 다른 테스트가 일치하면 누락 이름을 오류로 처리하지 않아 최종 명령이 잘못 성공했다.

#### 해결 방법

resolver가 error를 반환하는 case와 empty slice를 반환하는 case를 dedicated table test로 복구한다. stale `item.candidates`에 provider가 있어도 production snapshot의 `Queued`/`LongQueued`가 0인지 확인하고 exact anchored 실행 및 `go test -list` grep으로 존재를 검증한다.

Before:

```text
TestProviderSnapshotResolverErrorDoesNotUseStaleCandidate: 없음
```

After:

```go
func TestProviderSnapshotResolverErrorDoesNotUseStaleCandidate(t *testing.T) {
	// error/empty resolver cases both report zero candidate pressure.
}
```

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

- [ ] `apps/edge/internal/service/status_provider_test.go`: resolver error/empty table regression을 복구한다.
- [ ] `apps/edge/internal/service/status_provider_test.go`: stale candidates가 queued/long_queued에 포함되지 않음을 production snapshot으로 assertion한다.
- [ ] exact test existence와 targeted/race/Edge/preflight stdout/stderr를 review stub에 기록한다.

#### 테스트 작성

- 추가/복구: `TestProviderSnapshotResolverErrorDoesNotUseStaleCandidate`.
- assertion: error와 empty 모두 stale fallback 없이 zero pressure.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-go-cache-provider-snapshot-followup-2 go test -list '^TestProviderSnapshotResolverErrorDoesNotUseStaleCandidate$' ./apps/edge/internal/service | rg --fixed-strings 'TestProviderSnapshotResolverErrorDoesNotUseStaleCandidate'
GOCACHE=/tmp/iop-go-cache-provider-snapshot-followup-2 go test -count=20 ./apps/edge/internal/service -run '^TestProviderSnapshotResolverErrorDoesNotUseStaleCandidate$'
```

기대 결과: test name이 정확히 존재하고 error/empty case 20회 PASS.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/service/service.go` | REVIEW_REVIEW_PROVIDER_SNAPSHOT_SOURCE-1 |
| `apps/edge/internal/service/status_provider_test.go` | REVIEW_REVIEW_PROVIDER_SNAPSHOT_SOURCE-1, REVIEW_REVIEW_PROVIDER_SNAPSHOT_SOURCE-2, REVIEW_REVIEW_PROVIDER_SNAPSHOT_SOURCE-3, REVIEW_REVIEW_PROVIDER_SNAPSHOT_SOURCE-4 |

## 최종 검증

```bash
gofmt -d apps/edge/internal/service/service.go apps/edge/internal/service/status_provider_test.go
git diff --check
GOCACHE=/tmp/iop-go-cache-provider-snapshot-followup-2 go test -list '^TestProviderSnapshotResolverErrorDoesNotUseStaleCandidate$' ./apps/edge/internal/service | rg --fixed-strings 'TestProviderSnapshotResolverErrorDoesNotUseStaleCandidate'
GOCACHE=/tmp/iop-go-cache-provider-snapshot-followup-2 go test -count=1 ./apps/edge/internal/service -run '^TestProviderSnapshot(ResolverErrorDoesNotUseStaleCandidate|LiveCandidatePressureFollowsRefreshAcrossModelGroups|ConcurrentRefreshPressureUsesLiveCandidates|QueuedCountsCandidatePressureAcrossModelGroups|ReadsResourceState|IgnoresLegacyFallbackState)$'
GOCACHE=/tmp/iop-go-cache-provider-snapshot-followup-2 go test -race -count=5 -timeout 60s ./apps/edge/internal/service -run '^TestProviderSnapshot(LiveCandidatePressureFollowsRefreshAcrossModelGroups|ConcurrentRefreshPressureUsesLiveCandidates|QueuedCountsCandidatePressureAcrossModelGroups)$'
GOCACHE=/tmp/iop-go-cache-provider-snapshot-followup-2 go test -count=1 -timeout 60s ./apps/edge/...
GOCACHE=/tmp/iop-go-cache-provider-snapshot-followup-2 scripts/e2e-long-context-admission-smoke.sh --preflight --out-dir /tmp/iop-long-admission-smoke-provider-snapshot-followup-2
```

기대 결과: format/diff/test existence/targeted/race/Edge 전체 PASS. race detector 경고는 허용하지 않는다. preflight는 endpoint 가용 시 PASS하며 접근 불가 시 rc=3과 실제 evidence 경로를 기록하되 Go 검증 실패를 성공으로 취급하지 않는다. 모든 Go test는 fresh 실행이다.

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