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

# Provider Snapshot Runtime Refresh 선형화 보완 계획

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

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

## 배경

data race와 production snapshot lifecycle은 복구됐지만 runtime refresh와 status 조회가 교차할 때 `ListNodeSnapshots`가 old provider config와 new live candidate pressure를 섞어 반환한다. 리뷰 진단은 `available/capacity=1/queued=0` torn tuple을 10/10회 재현했으며, 현재 concurrent test는 이 조합을 허용해 통과했다. 이 후속은 queue/service reader-writer 경계를 하나로 선형화하고 exact old/new tuple 회귀로 SDD S07 evidence를 닫는다.

## 사용자 리뷰 요청 흐름

선택된 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_2.log`
- Archived review: `agent-task/m-provider-resource-admission-ownership-alignment/07+03_snapshot_source/code_review_local_G06_2.log`
- Verdict: FAIL
- Findings: Required 2, Suggested 1, Nit 0.
  - `ListNodeSnapshots`가 old `NodeStore`를 읽은 뒤 queue lock을 잡는 사이 runtime refresh가 new queue state로 전환되어 torn snapshot을 반환한다.
  - concurrent test가 enabled/disabled exact tuple을 검사하지 않아 `available/1/0` 조합을 허용한다.
  - `SetRuntimeConfig`가 queue lock 보유를 전제로 하는 `setProviderPoolPolicyLocked`를 재진입 함수로 잘못 설명하고 reconcile 로직을 복제한다.
- Affected files: `apps/edge/internal/service/service.go`, `apps/edge/internal/service/status_provider.go`, `apps/edge/internal/service/model_queue_snapshot.go`, `apps/edge/internal/service/status_provider_test.go`.
- Verification evidence:
  - forced queue-order diagnostic: FAIL 10/10; `torn snapshot: status="available" capacity=1 queued=0`, expected old `available/1/1` or new `disabled/0/0`.
  - exact resolver-error test existence와 targeted provider snapshot tests: PASS.
  - provider snapshot race suite: PASS, `ok iop/apps/edge/internal/service 2.883s`; data race는 없지만 logical atomicity를 검증하지 못했다.
  - fresh `go test -count=1 -timeout 120s ./apps/edge/...`: PASS.
  - provider-pool preflight: BLOCKED(rc=3), model/status endpoint 접근 불가; evidence=`/tmp/iop-long-admission-smoke-review-snapshot-g06-final`.
- Roadmap carryover: `snapshot-source`는 concurrent refresh 조회가 완전한 old 또는 new provider tuple만 반환하고 lifecycle/race 검증이 함께 통과할 때만 완료 대상으로 유지한다.
- Narrow reread allowed: 위 `plan_local_G06_2.log`, `code_review_local_G06_2.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/rules/common/rules-agent-spec.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-ops/skills/common/finalize-task-routing/SKILL.md`
- 활성/이전 루프: `agent-task/m-provider-resource-admission-ownership-alignment/07+03_snapshot_source/plan_local_G06_2.log`, `agent-task/m-provider-resource-admission-ownership-alignment/07+03_snapshot_source/code_review_local_G06_2.log`
- 선행 완료: `agent-task/archive/2026/07/m-provider-resource-admission-ownership-alignment/03+01,02_cross_group_wakeup/complete.log`
- 로드맵/SDD: `agent-roadmap/current.md`, `agent-roadmap/phase/operational-observability-provider-management/PHASE.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/index.md`, `agent-spec/runtime/provider-pool-config-refresh.md`, `agent-spec/control/control-plane-operations.md`, `agent-contract/index.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/status_provider.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/provider_resolution.go`, `apps/edge/internal/service/run_submit.go`, `apps/edge/internal/service/provider_pool.go`, `apps/edge/internal/service/provider_tunnel.go`
- tests: `apps/edge/internal/service/status_provider_test.go`, `apps/edge/internal/service/service_internal_test.go`
- 테스트 규칙/프로필: `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/testing-smoke.md`

### SDD 기준

- SDD는 `agent-roadmap/sdd/operational-observability-provider-management/provider-resource-admission-ownership-alignment/SDD.md`, 상태 `[승인됨]`, 잠금 해제다.
- 대상은 Acceptance Scenario S07 / Task `snapshot-source`다.
- S07은 실행·대기·반환 중 일반·long in-flight가 lease state와 같고 queued/long_queued가 live candidate pressure여야 한다.
- Evidence Map의 S07 row가 deterministic Edge provider resource/queue tests와 race 검증을 요구하므로 exact old/new tuple ordering 회귀, existing production lifecycle, race 반복을 구현 체크리스트와 최종 검증에 고정한다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`, `edge-smoke.md`, `testing-smoke.md`를 모두 읽었다.
- fresh Go test는 별도 `/tmp` GOCACHE와 `-count=1` 또는 명시 반복 횟수를 사용한다. format, scoped diff, exact test existence, targeted lifecycle, race, Edge 전체 회귀를 적용한다.
- runner는 `/config/workspace/iop`, branch `main`, HEAD `e4966b0b173d823f6fb9f0a995da5d6070915bcf`, Linux aarch64이고 worktree는 sibling subtask 변경으로 dirty다. 대상 네 파일의 diff만 추가하고 기존 변경과 repo-local `service.test` artifact를 되돌리거나 삭제하지 않는다.
- 전체 `git diff --check`는 sibling review artifact의 whitespace 때문에 실패하므로 대상 네 파일을 명시한 scoped command를 계약으로 사용한다.
- provider-pool preflight는 config check가 PASS했지만 model/status endpoint 접근 불가로 rc=3이다. 같은 명령을 재실행해 실제 stdout/stderr와 `/tmp` evidence 경로를 기록하되 S07 deterministic 검증 실패를 성공으로 바꾸지 않는다. live capacity 성공은 별도 `capacity-smoke` Task 책임이다.

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

- 현재 `TestProviderSnapshotConcurrentRefreshPressureUsesLiveCandidates`는 data race와 loose bound만 검사하고 full enabled/disabled tuple 원자성을 검사하지 않는다.
- `ListNodeSnapshots` reader가 old config를 캡처한 뒤 writer가 queue를 먼저 갱신하는 ordering을 강제하는 회귀가 없다.
- normal/long lifecycle, resolver error/empty, targeted race 검증은 존재하며 이번 후속에서 의미를 보존한다.

### 심볼 참조

- rename/remove 대상은 없다.
- `getSnapshotForNode` 호출부는 `status_provider.go`와 `provider_scheduling_advanced_test.go`에 있다. 기존 locking wrapper를 유지하고 production reader용 locked variant만 추가한다.
- `setProviderPoolPolicyLocked` 호출부는 runtime service와 provider-pool admission tests이며 queue lock 보유 계약을 유지한다.

### 분할 판단

- split decision policy를 평가했다. reader lock 경계, queue-locked builder와 exact tuple test는 같은 Edge service snapshot 불변식을 구현·검증하며 한 부분 실패가 다른 독립 산출물을 남기지 않는다.
- 기존 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`의 PASS로 충족됐다.
- API/계약 rollout, 다른 domain, 별도 외부 검증 소유권이 없어 추가 split은 인위적인 shared-lock 조정을 만든다.

### 범위 결정 근거

- queue scheduling, provider-pool policy 의미, disconnect/offline snapshot, config/contract/spec/roadmap/SDD 본문은 변경하지 않는다.
- `SetNodeStore`와 `SetModelCatalog`의 startup/legacy setter 의미를 재설계하지 않는다. production live refresh와 status snapshot의 `SetRuntimeConfig` 경계만 선형화한다.
- provider snapshot 외부 field와 proto를 바꾸지 않는다.
- unrelated sibling 변경과 `service.test`를 수정·삭제하지 않는다.

### 최종 라우팅

- `evaluation_mode=isolated-reassessment`
- build: status `routed`; closures `scope/context/verification/evidence/ownership/decision=true`; scores `scope_coupling=1`, `state_concurrency=2`, `blast_irreversibility=1`, `evidence_diagnosis=1`, `verification_complexity=1`; `local`, `G06`, `PLAN-local-G06.md`
- review: status `routed`; closures `scope/context/verification/evidence/ownership/decision=true`; scores `scope_coupling=1`, `state_concurrency=2`, `blast_irreversibility=1`, `evidence_diagnosis=1`, `verification_complexity=1`; `local`, `G06`, `CODE_REVIEW-local-G06.md`

## 의존 관계 및 구현 순서

1. queue/service lock order와 queue-locked snapshot builder를 먼저 정렬한다.
2. exact old/new tuple regression으로 forced ordering과 concurrent storm을 검증한다.
3. existing lifecycle/race/Edge 전체 검증과 live preflight를 실행한다.

## 구현 체크리스트

- [ ] `SetRuntimeConfig`와 `ListNodeSnapshots`를 queue → service 순서로 선형화하고 기존 queue-locked reconcile helper를 재사용해 config/resource/candidate-pressure가 완전한 old 또는 new 상태로 관찰되게 한다.
- [ ] enabled/disabled provider의 전체 snapshot tuple만 허용하는 forced-order 및 concurrent 회귀를 추가하고 exact existence, lifecycle, race, Edge 전체 검증과 provider-pool preflight 실제 출력을 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REVIEW_REVIEW_PROVIDER_SNAPSHOT_SOURCE-1] Runtime snapshot reader-writer 선형화

#### 문제

`apps/edge/internal/service/status_provider.go:27`은 service lock 아래 old store를 읽고 lock을 해제한 뒤 `:48`에서 queue lock을 잡는다. `apps/edge/internal/service/service.go:99-152`의 refresh writer가 그 사이 queue → service → queue state를 new 상태로 바꾸면 old provider config와 new live pressure가 섞인다. 또한 `service.go:110-151`은 이미 queue lock 보유를 요구하는 `setProviderPoolPolicyLocked`를 재진입 함수로 오해해 동일 reconcile을 복제한다.

#### 해결 방법

queue가 있는 production status 경로는 queue lock을 먼저 획득한 뒤 `runtimeConfigSnapshot`을 읽고, 같은 queue critical section에서 node/provider snapshot을 만든다. `model_queue_snapshot.go`에는 lock을 잡는 기존 wrapper와 caller-held-lock variant를 분리해 direct test 호출 계약을 보존한다. `SetRuntimeConfig`는 queue lock 아래 service snapshot을 교체한 뒤 기존 `setProviderPoolPolicyLocked`를 호출하고 queue가 없는 분기에서도 service-owned policy를 함께 저장한다.

Before (`status_provider.go:23-49`, `service.go:99-151`):

```go
store, _, _ := s.runtimeConfigSnapshot()
// ...
snap.ProviderSnapshots = s.queue.getSnapshotForNode(entry.NodeID, rec)

s.queue.mu.Lock()
// service update
// duplicated queue reconcile
```

After:

```go
s.queue.mu.Lock()
defer s.queue.mu.Unlock()
store, _, _ := s.runtimeConfigSnapshot()
// ...
snap.ProviderSnapshots = s.queue.getSnapshotForNodeLocked(entry.NodeID, rec)

// SetRuntimeConfig already holds s.queue.mu.
s.queue.setProviderPoolPolicyLocked(store, policy)
```

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

- [ ] `apps/edge/internal/service/service.go`: queue → service writer order를 유지하고 existing locked reconcile helper를 재사용한다.
- [ ] `apps/edge/internal/service/service.go`: queue nil 분기에서도 `providerPoolPolicy`를 service snapshot에 저장한다.
- [ ] `apps/edge/internal/service/status_provider.go`: queue-backed snapshot 전체를 queue → service 순서의 한 critical section으로 선형화한다.
- [ ] `apps/edge/internal/service/model_queue_snapshot.go`: existing locking wrapper와 caller-held-lock builder를 분리해 재잠금 없이 production snapshot을 만든다.

#### 테스트 작성

- 기존 테스트 유지: `apps/edge/internal/service/provider_scheduling_advanced_test.go`의 direct `getSnapshotForNode` 호출은 locking wrapper로 계속 통과해야 한다.
- 새 public API나 wire field는 없으므로 별도 contract test는 추가하지 않는다.

#### 중간 검증

```bash
gofmt -d apps/edge/internal/service/service.go apps/edge/internal/service/status_provider.go apps/edge/internal/service/model_queue_snapshot.go
GOCACHE=/tmp/iop-go-cache-provider-snapshot-atomic-followup go test -count=1 ./apps/edge/internal/service -run '^(TestRuntimeConfigSnapshotConcurrentReplace|TestProviderSnapshotConcurrentRefreshPressureUsesLiveCandidates|TestProviderSnapshotLiveCandidatePressureFollowsRefreshAcrossModelGroups)$'
```

기대 결과: format diff 없음, runtime refresh/snapshot/lifecycle targeted tests PASS.

### [REVIEW_REVIEW_REVIEW_PROVIDER_SNAPSHOT_SOURCE-2] Exact old/new tuple 회귀와 evidence 강화

#### 문제

`apps/edge/internal/service/status_provider_test.go:1165-1175`는 status가 이미 `disabled`일 때만 capacity/queued zero를 검사하고 그 외에는 queued upper bound만 검사한다. old `available/1/1`과 new `disabled/0/0` 사이의 torn `available/1/0`이 정상으로 통과하며, 현재 error channel은 동일 오류가 capacity를 넘으면 reader가 block될 수 있다.

#### 해결 방법

enabled provider는 `status=available`, configured capacity, holder in-flight, waiter queued tuple 전체를 검사하고 disabled provider는 모든 effective counter/capacity가 0인 tuple 전체를 검사한다. 각 reader는 첫 invalid tuple만 bounded result channel로 전달하고 종료한다. forced queue ordering 회귀 `TestProviderSnapshotRuntimeRefreshIsOldOrNew`를 추가하고 concurrent storm은 enabled/disabled 두 상태가 모두 실제 sample됐는지 확인한다.

Before (`status_provider_test.go:1165-1175`):

```go
if ps.GetStatus() == "disabled" {
	if ps.GetQueued() != 0 { /* ... */ }
	if ps.GetCapacity() != 0 { /* ... */ }
}
if ps.GetQueued() > 1 { /* ... */ }
```

After:

```go
switch ps.GetStatus() {
case "available":
	assertAvailableTuple(ps)
case "disabled":
	assertDisabledTuple(ps)
default:
	reportInvalidTuple(ps)
}
```

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

- [ ] `apps/edge/internal/service/status_provider_test.go`: `TestProviderSnapshotRuntimeRefreshIsOldOrNew` forced-order regression을 추가한다.
- [ ] `apps/edge/internal/service/status_provider_test.go`: concurrent storm을 exact enabled/disabled tuple과 양쪽 상태 sample assertion으로 강화한다.
- [ ] `apps/edge/internal/service/status_provider_test.go`: reader당 최대 한 오류만 전달하는 bounded failure collection으로 hang 가능성을 제거한다.
- [ ] `apps/edge/internal/service/status_provider_test.go`: 기존 normal/long lifecycle과 resolver error/empty assertions를 유지한다.

#### 테스트 작성

- 추가: `apps/edge/internal/service/status_provider_test.go`의 `TestProviderSnapshotRuntimeRefreshIsOldOrNew`.
- fixture: capacity 1 enabled/disabled immutable stores, live resolver pending item, queue-held writer/reader ordering 또는 동등한 bounded barrier.
- assertion: 반환 tuple은 old `available/1/1` 또는 new `disabled/0/0` 중 하나만 허용하며 torn tuple은 즉시 실패한다.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-go-cache-provider-snapshot-atomic-followup go test -list '^TestProviderSnapshotRuntimeRefreshIsOldOrNew$' ./apps/edge/internal/service | rg --fixed-strings 'TestProviderSnapshotRuntimeRefreshIsOldOrNew'
GOCACHE=/tmp/iop-go-cache-provider-snapshot-atomic-followup go test -count=20 -timeout 60s ./apps/edge/internal/service -run '^TestProviderSnapshot(RuntimeRefreshIsOldOrNew|ConcurrentRefreshPressureUsesLiveCandidates)$'
GOCACHE=/tmp/iop-go-cache-provider-snapshot-atomic-followup go test -race -count=5 -timeout 60s ./apps/edge/internal/service -run '^TestProviderSnapshot(RuntimeRefreshIsOldOrNew|ConcurrentRefreshPressureUsesLiveCandidates|LiveCandidatePressureFollowsRefreshAcrossModelGroups)$'
```

기대 결과: exact test name 존재, old/new tuple 회귀 20회 PASS, race detector 경고·deadlock 없이 PASS.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/service/service.go` | REVIEW_REVIEW_REVIEW_PROVIDER_SNAPSHOT_SOURCE-1 |
| `apps/edge/internal/service/status_provider.go` | REVIEW_REVIEW_REVIEW_PROVIDER_SNAPSHOT_SOURCE-1 |
| `apps/edge/internal/service/model_queue_snapshot.go` | REVIEW_REVIEW_REVIEW_PROVIDER_SNAPSHOT_SOURCE-1 |
| `apps/edge/internal/service/status_provider_test.go` | REVIEW_REVIEW_REVIEW_PROVIDER_SNAPSHOT_SOURCE-2 |

## 최종 검증

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

기대 결과: format/scoped diff/exact existence/targeted/race/Edge 전체 PASS. preflight는 endpoint 가용 시 PASS하고, 동일 endpoint 미가용 시 rc=3과 실제 blocker stdout/stderr/evidence 경로를 기록한다.

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