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

# Provider Snapshot Public Reader 회귀 증거 보완 계획

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

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

## 배경

production snapshot 선형화와 Go 회귀는 통과하지만 forced-order 테스트가 public `ListNodeSnapshots()`의 queue lock 획득을 우회해 reader-side 회귀를 잡지 못한다. concurrent 테스트도 fallback provider의 available sample을 shared provider로 대체할 수 있고 expected provider 누락을 허용한다. production 코드는 유지하고 public reader barrier, provider별 presence/exact tuple, fallback 양 상태 표본만 보완해 SDD S07 evidence를 신뢰 가능하게 만든다.

## 사용자 리뷰 요청 흐름

선택된 Milestone lock 결정만 active `CODE_REVIEW-local-G06.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.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_G05_4.log`
- Archived review: `agent-task/m-provider-resource-admission-ownership-alignment/07+03_snapshot_source/code_review_local_G06_4.log`
- Verdict: FAIL
- Findings: Required 3, Suggested 0, Nit 1.
  - forced-order fixture가 queue lock을 직접 잡고 internal `buildSnapshotLocked()`를 호출해 public reader lock 회귀를 잡지 못하며 첫 transition 뒤 stale disabled 반복만 수행한다.
  - `seenAvailable`이 always-available shared provider만으로 충족되어 fallback의 available/disabled 양 상태 sample을 증명하지 못한다.
  - concurrent reader가 반환된 provider만 검증해 expected provider snapshot 누락을 허용한다.
  - 사용되지 않는 `snapReport`, `assertAvailableTuple`, `assertDisabledTuple`과 실제 참조와 다른 보존 설명이 남아 있다.
- Affected files: `apps/edge/internal/service/status_provider_test.go`.
- Verification evidence:
  - reviewer fresh format/diff, exact discovery, exact/concurrent 20회, targeted lifecycle, race 5회, `go test -count=1 -timeout 120s ./apps/edge/...`는 PASS했다.
  - provider-pool preflight는 config check PASS 후 configured model/status endpoint 접근 불가로 BLOCKED(rc=3), evidence=`/tmp/iop-long-admission-smoke-provider-snapshot-review5`.
- Roadmap carryover: `snapshot-source`는 public `ListNodeSnapshots` reader lock acquisition을 강제한 deterministic old/new tuple과 provider별 exact presence/state 검증이 반복·race·Edge 전체 검증과 함께 통과할 때만 완료 대상으로 유지한다.
- Narrow reread allowed: 위 `plan_local_G05_4.log`, `code_review_local_G06_4.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_G05_4.log`, `agent-task/m-provider-resource-admission-ownership-alignment/07+03_snapshot_source/code_review_local_G06_3.log`, `agent-task/m-provider-resource-admission-ownership-alignment/07+03_snapshot_source/code_review_local_G06_4.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`
- production context: `apps/edge/internal/service/service.go`, `apps/edge/internal/service/status_provider.go`, `apps/edge/internal/service/model_queue_snapshot.go`
- 변경 대상 test 전체: `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`
- plan/review template: `agent-ops/skills/common/plan/templates/review-stub-template.md`, `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`

### SDD 기준

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-resource-admission-ownership-alignment/SDD.md`, 상태 `[승인됨]`, 잠금 `해제`.
- 대상: Acceptance Scenario S07 / Milestone 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 검증을 요구한다. 따라서 public reader lock ordering, provider별 exact presence/tuple, fallback 양 상태 관찰을 구현 체크리스트에 넣고 반복·race·Edge 전체 검증을 최종 evidence로 고정한다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/testing-smoke.md`가 존재해 모두 읽었다.
- fresh Go test는 `/tmp` GOCACHE와 `-count=1` 또는 명시 반복 횟수를 사용한다. 변경 파일 format/diff, exact test existence, deterministic/concurrent 20회, snapshot lifecycle, race 5회, Edge 전체 회귀를 적용한다.
- 테스트 환경 프리플라이트: runner `/config/workspace/iop`, branch `main`, HEAD `546a3f8a123cd2fb6f6faf3dbd9c655f71c3d0fd`, Linux aarch64, Go `1.26.2`, sibling subtask 변경이 있는 dirty worktree다. source는 현재 checkout을 그대로 검증하며 sibling 변경과 repo-local `service.test`를 수정하지 않는다. config check는 PASS했다. configured model/status endpoint는 접근 불가해 preflight rc=3이며 evidence는 `/tmp/iop-long-admission-smoke-provider-snapshot-review5`다. 이는 별도 `capacity-smoke` 책임의 환경 blocker이며 test-only 후속 구현 결정은 막지 않는다.
- reviewer fresh run에서 exact/concurrent 20회, targeted snapshot suite, race 5회와 Edge 전체가 PASS했으므로 명령 경로와 현재 compile 상태는 확인됐다. green 출력만으로 assertion 공백을 닫지 않고 source invariant를 함께 리뷰한다.

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

- forced-order fixture는 public `ListNodeSnapshots` lock acquisition을 실행하지 않아 reader가 queue lock을 잃는 회귀를 검출하지 못한다.
- internal 200회 루프는 enabled pressure를 재구성하지 않아 첫 old→new transition 뒤 disabled→disabled만 반복한다.
- concurrent storm의 available sample은 fallback provider에 귀속되지 않고, 각 read에서 두 expected provider가 모두 존재하는지도 검사하지 않는다.
- reader별 first-error bounded/nonblocking delivery와 main-goroutine assertion은 현재 구현되어 있으며 후속에서 보존한다.
- normal/long lifecycle, resolver error/empty, exact present-provider tuple, race와 Edge 전체 회귀는 기존 테스트가 유지한다.

### 심볼 참조

- 제거 대상 test-local symbol: `snapReport`, `assertAvailableTuple`, `assertDisabledTuple`.
- `rg` 결과 세 symbol의 호출자는 없고 정의/설명만 남아 있다. production call site와 import chain 변경은 없다.

### 분할 판단

- split decision policy를 재평가했다. 기존 split subtask `07+03_snapshot_source`를 유지하며 새 sibling을 만들지 않는다.
- public barrier와 concurrent provider presence/state는 같은 테스트 파일의 동일 snapshot evidence 불변식을 보완한다. API/production 소유권, 별도 검증 전략, 독립 산출물 경계가 없어 추가 split은 fixture/helper 조정을 인위적으로 분리한다.
- predecessor index `03`은 `agent-task/archive/2026/07/m-provider-resource-admission-ownership-alignment/03+01,02_cross_group_wakeup/complete.log`의 PASS로 충족됐다.

### 범위 결정 근거

- `apps/edge/internal/service/status_provider_test.go`만 수정한다.
- production lock/order 구현, provider policy, queue scheduling, config/contract/spec/roadmap/SDD 본문은 변경하지 않는다.
- sibling subtask 파일, repo-local `service.test`, external endpoint/runtime을 수정·삭제하지 않는다.
- live capacity 1 field smoke는 별도 `capacity-smoke` Task 책임으로 유지한다.

### 최종 라우팅

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

## 의존 관계 및 구현 순서

1. forced-order fixture를 public reader barrier와 한 번의 fresh old→new transition으로 교체한다.
2. concurrent reader에 provider presence와 fallback별 state sample을 추가하고 dead test symbol을 정리한다.
3. 반복·race·Edge 전체 검증과 provider-pool preflight를 실행한다.

## 구현 체크리스트

- [ ] forced-order fixture가 service lock barrier 아래 production `ListNodeSnapshots` reader의 queue lock 획득을 bounded probe로 확인하고 exact old `available/1/1`과 writer 완료 뒤 new `disabled/0/0`을 public surface에서 검증하며 fresh `-count=20` 반복으로 재현되게 한다.
- [ ] concurrent storm이 매 read마다 shared/fallback provider 존재와 전체 tuple을 exact 검증하고 fallback provider 자체의 available/disabled 양 상태를 각각 sample하며 first-error bounded collection을 보존하고 dead test symbol을 정리한 뒤 targeted·race·Edge 전체 검증과 provider-pool preflight 실제 출력을 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_PROVIDER_SNAPSHOT_SOURCE-1] Public reader deterministic lock barrier

#### 문제

`apps/edge/internal/service/status_provider_test.go:1685-1713`은 main goroutine이 `svc.queue.mu`를 직접 잡고 test goroutine이 internal `buildSnapshotLocked()`를 호출한다. 이 구조는 public `ListNodeSnapshots()`의 queue lock 획득이 제거되어도 통과하며, 첫 disabled apply 뒤 enabled pressure를 복구하지 않아 나머지 internal iteration은 같은 disabled state만 읽는다.

#### 해결 방법

internal 200회 루프를 제거하고 한 test invocation마다 fresh enabled holder/waiter를 만든다. `svc.mu` write lock으로 public reader를 service snapshot 직전에 멈추고, reader goroutine이 `ListNodeSnapshots()`를 호출해 queue lock을 획득했음을 bounded `svc.queue.mu.TryLock()` poll로 확인한다. 그 뒤 writer goroutine을 시작하고 service lock을 풀어 reader가 exact old tuple을 반환한 뒤 writer가 disabled config를 적용하게 하며, writer 완료 후 public surface에서 exact new tuple을 확인한다. 외부 `go test -count=20`이 fresh fixture 반복을 소유한다.

Before (`status_provider_test.go:1685-1713`):

```go
svc.queue.mu.Lock()
resultCh := make(chan snapshotTuple, 1)
go func() {
	defer svc.queue.mu.Unlock()
	snaps := svc.buildSnapshotLocked()
	resultCh <- snapshotTupleFromSnapshots(snaps)
}()
runtime.Gosched()
svc.SetRuntimeConfig(disabledStore, catalog, groupPolicy{})
```

After:

```go
svc.mu.Lock()
readerResult := make(chan []NodeSnapshot, 1)
go func() {
	readerResult <- svc.ListNodeSnapshots()
}()
waitUntilQueueLockHeld(t, svc.queue)

writerDone := make(chan struct{})
go func() {
	svc.SetRuntimeConfig(disabledStore, catalog, groupPolicy{})
	close(writerDone)
}()
svc.mu.Unlock()

assertExactSnapshotTuple(t, <-readerResult, nodeID, "prov-x", expectedOld)
<-writerDone
assertExactSnapshotTuple(t, svc.ListNodeSnapshots(), nodeID, "prov-x", expectedNew)
```

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

- [ ] `apps/edge/internal/service/status_provider_test.go`: public `ListNodeSnapshots` reader와 service-lock/queue-lock bounded barrier를 구성한다.
- [ ] `apps/edge/internal/service/status_provider_test.go`: internal stale-state 200회 루프를 fresh invocation 단일 transition으로 바꾸고 old/new exact public tuple을 각각 검증한다.
- [ ] `apps/edge/internal/service/status_provider_test.go`: timeout/failure에서도 held lock과 goroutine이 남지 않도록 barrier channel과 cleanup을 bounded하게 만든다.

#### 테스트 작성

- 수정: `TestProviderSnapshotRuntimeRefreshIsOldOrNew`.
- fixture: explicit enabled/disabled provider store, capacity 1 holder, live queued waiter, service write-lock barrier.
- assertion: public reader가 queue lock을 잡은 뒤 old `available/1/1/0/0`, writer 완료 뒤 new `disabled/0/0/0/0`이며 direct `buildSnapshotLocked()` test call은 없다.

#### 중간 검증

```bash
gofmt -d apps/edge/internal/service/status_provider_test.go
GOCACHE=/tmp/iop-go-cache-provider-snapshot-public-reader-followup go test -list '^TestProviderSnapshotRuntimeRefreshIsOldOrNew$' ./apps/edge/internal/service | rg --fixed-strings 'TestProviderSnapshotRuntimeRefreshIsOldOrNew'
GOCACHE=/tmp/iop-go-cache-provider-snapshot-public-reader-followup go test -count=20 -timeout 60s ./apps/edge/internal/service -run '^TestProviderSnapshotRuntimeRefreshIsOldOrNew$'
```

기대 결과: format diff 없음, exact test name 존재, fresh public reader old/new ordering 회귀 20회 PASS.

### [REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_PROVIDER_SNAPSHOT_SOURCE-2] Provider별 exact presence와 fallback state sample

#### 문제

`apps/edge/internal/service/status_provider_test.go:1311-1350`은 반환된 provider만 순회하므로 expected provider 누락을 실패로 처리하지 않는다. `seenAvailable`도 provider id를 구분하지 않아 항상 available인 `prov-shared`만으로 충족되고, `status_provider_test.go:1285`의 `snapReport`와 test-local tuple helper 둘은 호출자가 없다.

#### 해결 방법

각 snapshot을 provider id map으로 만든 뒤 shared/fallback 두 id의 존재를 먼저 요구하고 exact expected tuple을 각각 검증한다. fallback tuple이 exact enabled일 때만 `fallbackSeenAvailable`, exact disabled일 때만 `fallbackSeenDisabled`를 갱신한다. 누락·중복·tuple mismatch는 모두 해당 reader의 첫 `snapshotFailure`로 nonblocking 전달한 뒤 즉시 종료한다. 호출자 없는 `snapReport`, `assertAvailableTuple`, `assertDisabledTuple`과 stale 보존 설명을 제거한다.

Before (`status_provider_test.go:1311-1350`):

```go
for _, ps := range snaps[0].ProviderSnapshots {
	pid := ps.GetId()
	// present providers only
	if matchAnySnapshotTuple(got, allowed...) {
		if got.status == "available" {
			seenAvailable = true
		}
		continue
	}
}
```

After:

```go
byID, err := exactProviderTuples(snaps, nodeID, "prov-shared", "prov-fallback")
if err != nil {
	reportFirstSnapshotFailure(errCh, readerID, err)
	return
}
if err := validateSnapshotTuple(byID["prov-shared"], expectedShared); err != nil {
	reportFirstSnapshotFailure(errCh, readerID, err)
	return
}
switch fallback := byID["prov-fallback"]; {
case validateSnapshotTuple(fallback, expectedFallbackEnabled) == nil:
	fallbackSeenAvailable = true
case validateSnapshotTuple(fallback, expectedFallbackDisabled) == nil:
	fallbackSeenDisabled = true
default:
	reportFirstSnapshotFailure(errCh, readerID, fmt.Errorf("fallback tuple mismatch"))
	return
}
```

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

- [ ] `apps/edge/internal/service/status_provider_test.go`: 매 read의 node/provider presence와 duplicate/missing id를 exact 검증한다.
- [ ] `apps/edge/internal/service/status_provider_test.go`: fallback provider 전용 available/disabled flags를 exact tuple match 뒤 갱신한다.
- [ ] `apps/edge/internal/service/status_provider_test.go`: reader당 첫 오류, bounded/nonblocking delivery, main-goroutine assertion을 보존한다.
- [ ] `apps/edge/internal/service/status_provider_test.go`: 호출자 없는 `snapReport`, `assertAvailableTuple`, `assertDisabledTuple`과 stale 설명을 제거한다.

#### 테스트 작성

- 수정: `TestProviderSnapshotConcurrentRefreshPressureUsesLiveCandidates`.
- assertion: 매 read에 `prov-shared`와 `prov-fallback`이 정확히 존재하고 shared는 exact available, fallback은 exact available/disabled 중 하나이며 fallback 양 상태가 storm 동안 각각 관찰된다.
- failure-path: missing provider와 tuple mismatch 모두 reader별 첫 bounded error로 main goroutine에 전달된다.

#### 중간 검증

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

기대 결과: exact/concurrent 20회와 race 5회가 missing provider, tuple mismatch, deadlock, race detector 경고 없이 PASS.

## 수정 파일 요약

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

## 최종 검증

```bash
gofmt -d apps/edge/internal/service/status_provider_test.go
git diff --check -- apps/edge/internal/service/status_provider_test.go
GOCACHE=/tmp/iop-go-cache-provider-snapshot-public-reader-followup go test -list '^TestProviderSnapshotRuntimeRefreshIsOldOrNew$' ./apps/edge/internal/service | rg --fixed-strings 'TestProviderSnapshotRuntimeRefreshIsOldOrNew'
GOCACHE=/tmp/iop-go-cache-provider-snapshot-public-reader-followup go test -count=20 -timeout 60s ./apps/edge/internal/service -run '^TestProviderSnapshot(RuntimeRefreshIsOldOrNew|ConcurrentRefreshPressureUsesLiveCandidates)$'
GOCACHE=/tmp/iop-go-cache-provider-snapshot-public-reader-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-public-reader-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-public-reader-followup go test -count=1 -timeout 120s ./apps/edge/...
GOCACHE=/tmp/iop-go-cache-provider-snapshot-public-reader-followup scripts/e2e-long-context-admission-smoke.sh --preflight --out-dir /tmp/iop-long-admission-smoke-provider-snapshot-public-reader-followup
```

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

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