<!-- task=m-provider-resource-admission-ownership-alignment/10+08_configured_snapshot_catalog plan=1 tag=REVIEW_REFACTOR -->

# Snapshot catalog race and path-coverage follow-up

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

구현 에이전트는 소스·테스트 변경, 구현 검증, active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성까지만 수행한다. 모두 채운 뒤 active pair를 유지하고 구현 실행을 종료한다. 이 저장 상태가 runtime의 `review-ready` 신호이며 별도 handoff 파일·사용자 메시지를 만들지 않는다. code-review 실행, 역할 전환, 판정, review 전용 섹션 수정, archive/log rename, `USER_REVIEW.md`·`complete.log` 작성, 후속 pair 생성·구현은 금지한다. runtime은 별도 review-agent instance를 배정하며 같은 agent의 새 turn·resume·compaction·역할 변경으로 대체하지 않는다. 선택 Milestone의 `구현 잠금 > 결정 필요`만 정확한 연결 근거와 함께 `사용자 리뷰 요청`에 기록하고 같은 file-state handoff를 사용한다. 구현 중 사용자에게 직접 묻거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경·secret·서비스·일반 scope 변경·자동 후속으로 닫을 수 있는 evidence gap은 user-review 사유가 아니다.

## 배경

최초 구현은 configured `NodeStore` catalog을 snapshot identity 원본으로 바꾸었지만, no-queue builder가 mutex로 capture한 store 대신 `s.nodeStore`를 다시 읽어 runtime config swap과 경합한다. 또한 `noQueue` 테스트가 queue-backed 경로를 타고 reconnect fixture도 실제 disconnect 후 reconnect를 만들지 않아 핵심 불변식이 검증되지 않았다. 이 follow-up은 재현된 race와 경로 커버리지 공백만 닫는다.

## 사용자 리뷰 요청 흐름

선택 Milestone 잠금 결정만 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 중 직접 user prompt는 금지하며 code-review가 요청 검증과 실제 `USER_REVIEW.md` 작성을 소유한다.

## Archive Evidence Snapshot

- 이전 Plan: `agent-task/m-provider-resource-admission-ownership-alignment/10+08_configured_snapshot_catalog/plan_local_G06_0.log`
- 이전 Review: `agent-task/m-provider-resource-admission-ownership-alignment/10+08_configured_snapshot_catalog/code_review_local_G06_0.log`
- 판정: FAIL
- 미해결: Required 3건, Suggested 0건, Nit 0건.
- Required: no-queue `s.nodeStore` data race, `noQueue` fixture의 queue-backed 오라우팅, disconnect 없는 가상 reconnect 검증.
- 영향 파일: `apps/edge/internal/service/status_provider.go`, `apps/edge/internal/service/status_provider_test.go`; 기존 race regression 근거는 `apps/edge/internal/service/service_internal_test.go`.
- 검증 근거: `go test -race -count=1 ./apps/edge/internal/service`는 `TestRuntimeConfigSnapshotConcurrentReplace`에서 `status_provider.go:57` read / `service.go:117` write race로 FAIL. `noQueue` 단독 coverage는 `buildSnapshotNoQueue=0.0%`, `buildSnapshotLocked=92.9%`. 전체 Edge 회귀와 대상 test race 10회는 PASS. 전체 Go 회귀의 유일 실패는 변경 범위 밖 `apps/node/internal/adapters/cli` 기준선 3건.
- Roadmap carryover: 이 child는 SDD S14의 configured catalog/connectivity join 부분 근거이며 `offline-snapshot` Task 완료를 단독 주장하지 않으므로 `Roadmap Targets`를 사용하지 않는다.

## 분석 결과

### 읽은 파일

- `agent-task/m-provider-resource-admission-ownership-alignment/10+08_configured_snapshot_catalog/plan_local_G06_0.log`, `code_review_local_G06_0.log`
- `apps/edge/internal/service/status_provider.go`, `status_provider_test.go`
- `apps/edge/internal/service/service.go`, `service_internal_test.go:145-264`, `model_queue_snapshot.go`, `model_queue_types.go:240-345`, `provider_resolution.go:380-440`
- `apps/edge/internal/node/store.go`, `store_test.go`, `registry.go`, `mapper.go`
- `apps/edge/internal/controlplane/connector.go:350-385`, `connector_test.go:218-311`
- `apps/edge/internal/opsconsole/console.go:208-220`, `apps/edge/internal/service/control_command.go:60-105`, `run_command_test.go:200-405`
- `apps/edge/internal/bootstrap/runtime_refresh_test.go:820-875`, `apps/edge/internal/transport/server.go:200-250`
- `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/runtime/edge-node-execution.md`, `agent-spec/runtime/provider-pool-config-refresh.md`, `agent-spec/control/control-plane-operations.md`
- `agent-contract/inner/edge-node-runtime-wire.md`, `agent-contract/inner/control-plane-edge-wire.md`
- `agent-ops/rules/project/domain/edge/rules.md`, `agent-ops/rules/project/domain/testing/rules.md`
- `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: S14 / Milestone Task `offline-snapshot`.
- Evidence Map: S14는 Edge connectivity/snapshot deterministic test와 최종 race evidence를 요구한다. 이 follow-up은 configured identity 유지, real disconnect/reconnect connectivity join, race-free snapshot read를 검증하지만 offline effective tuple·Control Plane mapping과 Task 전체 `Roadmap Completion`은 후속 child에 남긴다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`, `edge-smoke.md`, `testing-smoke.md`를 적용한다.
- fresh `-count=1`, Go race detector, `/tmp` coverage artifact를 사용한다. repo 내 tool artifact를 남기지 않는다.
- 필수 gate는 대상 snapshot test, service 전체 race, Edge 전체 회귀다. 이 child는 wire mapping·offline effective tuple을 바꾸지 않아 live provider/full-cycle smoke는 후속 14/15 child의 closure에 남긴다.
- 전체 `go test -count=1 ./...`에서 현재 변경 범위 밖 CLI adapter 기준선 3건이 재현됐으며, 이 follow-up의 성공 gate는 Edge 범위와 race로 제한한다.

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

- `buildSnapshotNoQueue`: 현 `noQueue` subtest에서 0.0%; nil event bus와 `svc.queue == nil` assertion으로 직접 커버해야 한다.
- runtime config concurrent replace: 기존 `TestRuntimeConfigSnapshotConcurrentReplace`가 적절한 regression fixture지만 race 실행에서 현 구현이 실패한다.
- same-identity reconnect: 현 테스트는 offline→first connect만 보며 connected→disconnect→new generation reconnect를 보지 않는다.

### 심볼 참조

- 변경 심볼: `buildSnapshotNoQueue`, `buildSnapshotLocked`, `ListNodeSnapshots`, `NodeSnapshot.Connected`.
- `ListNodeSnapshots` 소비자는 service/opsconsole/Control Plane connector/test에 있다. 이 follow-up은 DTO/wire 형식을 바꾸지 않는다.
- renamed/removed symbol: 없음.

### 분할 판단

- 선택 task는 기존 `10+08_configured_snapshot_catalog` follow-up 하나로 유지한다.
- immediate sibling set: `10+08_configured_snapshot_catalog` 1개. write set은 `status_provider.go`, `status_provider_test.go`며 두 Required가 같은 snapshot builder/test fixture를 공유하므로 분할하지 않는다.
- predecessor 08은 `agent-task/archive/2026/07/m-provider-resource-admission-ownership-alignment/08_disconnect_exclusion/complete.log`로 충족된다.

### 범위 결정 근거

- 포함: captured store 일관 사용, 실제 no-queue/queue-backed 경로, disconnect/reconnect generation test, race/Edge 회귀.
- 제외: offline provider effective tuple(`14+10`), Control Plane `Connected` mapping(`15+14`), queue reconnect pump(`11+08,13,15`), proto/contract/spec 갱신.
- `NodeStore.All()`의 configured Index 정렬은 현 범위에서 유지하고 추가 API 변경을 하지 않는다.

### 최종 라우팅

- `evaluation_mode=isolated-reassessment`.
- build: 폐쇄성 `scope/context/verification/evidence/ownership/decision=true`; race 지점·수정 파일·검증 명령·소유권이 현 범위 안에서 결정적이다. scores=`scope_coupling:1,state_concurrency:2,blast_irreversibility:0,evidence_diagnosis:1,verification_complexity:1`; `local`, `G05`, `PLAN-local-G05.md`.
- review: 폐쇄성 `scope/context/verification/evidence/ownership/decision=true`; race 소실, 실제 path 커버리지, reconnect 전이, Edge 회귀를 local에서 판정할 수 있다. scores=`scope_coupling:1,state_concurrency:2,blast_irreversibility:0,evidence_diagnosis:1,verification_complexity:1`; `local`, `G05`, `CODE_REVIEW-local-G05.md`.

## 구현 체크리스트

- [ ] snapshot builder가 `runtimeConfigSnapshot()`으로 capture한 store를 nil 판정·catalog 순회·record lookup에 일관하게 사용해 concurrent replace race를 제거한다.
- [ ] 실제 no-queue/queue-backed configured snapshot과 connected→disconnect→same-identity reconnect를 deterministic test로 검증한다.
- [ ] target coverage, service race, Edge 회귀, formatting/diff 검증 결과를 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우고 active pair를 유지한 채 구현 실행을 종료한다. 이 저장 상태가 runtime의 review-ready 신호이며 별도 handoff 파일·사용자 메시지를 만들지 않는다. 구현 에이전트는 code-review를 실행하거나 review 전용 작업을 수행하지 않는다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REFACTOR-1] Runtime snapshot store consistency

문제: `apps/edge/internal/service/status_provider.go:56-60`이 lock 보호로 얻은 local `store`와 무잠금 `s.nodeStore`를 혼용해 concurrent `SetRuntimeConfig` write와 race한다.

해결 방법:

```go
// Before: apps/edge/internal/service/status_provider.go:56
store, _, _ := s.runtimeConfigSnapshot()
if s.nodeStore == nil { ... }
records := s.nodeStore.All()

// After
store, _, _ := s.runtimeConfigSnapshot()
if store == nil { ... }
records := store.All()
```

queue-backed builder도 같은 local store를 nil 판정·순회·lookup에 사용해 한 snapshot 내의 catalog identity를 고정한다.

수정 파일 및 체크리스트:

- [ ] `apps/edge/internal/service/status_provider.go`: 두 builder의 direct `s.nodeStore` read를 제거하고 captured store만 사용.
- [ ] `apps/edge/internal/service/service_internal_test.go`: 소스 변경 없이 기존 `TestRuntimeConfigSnapshotConcurrentReplace`를 race regression으로 사용; 수정이 필요하면 이 불변식을 약화하지 않는 최소 assertion만 보강.

테스트 작성: 기존 `TestRuntimeConfigSnapshotConcurrentReplace`가 no-queue `ListNodeSnapshots()`와 runtime swap을 동시 실행하므로 이를 `-race` regression으로 고정한다. 새 별도 concurrency harness는 만들지 않는다.

### [REVIEW_REFACTOR-2] Real no-queue and reconnect coverage

문제: `apps/edge/internal/service/status_provider_test.go:1773-1916`의 `noQueue`는 queue를 활성화하고 reconnect는 first connect만 수행한다.

해결 방법:

```go
// Before: apps/edge/internal/service/status_provider_test.go:1809
bus := edgeevents.NewBus()
svc := New(reg, bus)

// After
svc := New(reg, nil)
if svc.queue != nil { t.Fatal("expected no-queue service") }
```

no-queue test는 configured `providers[]` catalog·identity·Index order·Connected를 assertion하고, legacy adapter snapshot parity를 이 child의 새 요구로 추가하지 않는다. reconnect test는 node-b를 연결한 뒤 `Unregister`, offline snapshot, 새 `NodeEntry` register 순서로 실행하고 generation 증가·identity·order·Connected 회복을 검증한다.

수정 파일 및 체크리스트:

- [ ] `apps/edge/internal/service/status_provider_test.go`: actual no-queue fixture와 queue-backed fixture를 명시적으로 구분.
- [ ] `apps/edge/internal/service/status_provider_test.go`: connected→disconnect→new generation reconnect를 동일 Node ID로 검증.

테스트 작성: `TestListNodeSnapshotsIncludesConfiguredOfflineNodes` 하위 테스트를 수정해 actual builder path, configured provider catalog, stable order, real reconnect transition을 검증한다.

## 의존 관계 및 구현 순서

- predecessor 08은 `agent-task/archive/2026/07/m-provider-resource-admission-ownership-alignment/08_disconnect_exclusion/complete.log`로 충족됐다.
- REVIEW_REFACTOR-1을 먼저 적용한 뒤 REVIEW_REFACTOR-2 fixture로 no-queue/race 경계를 검증한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/service/status_provider.go` | REVIEW_REFACTOR-1 |
| `apps/edge/internal/service/status_provider_test.go` | REVIEW_REFACTOR-2 |
| `apps/edge/internal/service/service_internal_test.go` | REVIEW_REFACTOR-1 regression fixture; 기본은 무변경 |

## 최종 검증

- `gofmt -d apps/edge/internal/service/status_provider.go apps/edge/internal/service/status_provider_test.go apps/edge/internal/service/service_internal_test.go` → 출력 없음.
- `git diff --check -- apps/edge/internal/service/status_provider.go apps/edge/internal/service/status_provider_test.go apps/edge/internal/service/service_internal_test.go` → PASS.
- `go test -count=1 -coverprofile=/tmp/iop-configured-snapshot-noqueue.cover ./apps/edge/internal/service -run '^TestListNodeSnapshotsIncludesConfiguredOfflineNodes/noQueue$'` → PASS.
- `go tool cover -func=/tmp/iop-configured-snapshot-noqueue.cover | rg 'status_provider.go:[0-9]+:\s+buildSnapshotNoQueue\s+[1-9][0-9]*\.[0-9]+%'` → `buildSnapshotNoQueue` non-zero coverage 출력.
- `go test -race -count=1 ./apps/edge/internal/service -run '^(TestRuntimeConfigSnapshotConcurrentReplace|TestListNodeSnapshotsIncludesConfiguredOfflineNodes)$'` → PASS, race warning 0.
- `go test -race -count=1 ./apps/edge/internal/service` → PASS, race warning 0.
- `go test -count=1 ./apps/edge/...` → PASS.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우고 active pair를 유지한 채 구현 실행을 종료한다. 이 저장 상태가 runtime의 `review-ready` 신호이며 별도 handoff 파일·사용자 메시지를 만들지 않는다. 구현 에이전트는 code-review를 실행하거나 review 전용 작업을 수행하지 않고, runtime은 별도 review 에이전트 instance에 즉시 인계한다.
